├── demo
├── __init__.py
├── app.py
└── css.css
├── backend
└── gradio_log
│ ├── __init__.py
│ └── log.py
├── frontend
├── tsconfig.json
├── package.json
└── Index.svelte
├── assets
├── dynamic.gif
└── static.png
├── .gitignore
├── README.md
└── pyproject.toml
/demo/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/gradio_log/__init__.py:
--------------------------------------------------------------------------------
1 | from .log import Log
2 |
3 | __all__ = ["Log"]
4 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 | }
--------------------------------------------------------------------------------
/assets/dynamic.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/louis-she/gradio-log/HEAD/assets/dynamic.gif
--------------------------------------------------------------------------------
/assets/static.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/louis-she/gradio-log/HEAD/assets/static.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .eggs/
2 | dist/
3 | *.pyc
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 | __tmp/*
8 | *.pyi
9 | node_modules
10 | test.log
11 | .DS_Store
12 | backend/gradio_log/templates
13 | demo/space.py
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gradio_log",
3 | "version": "0.1.13",
4 | "description": "Gradio UI packages",
5 | "type": "module",
6 | "author": "",
7 | "license": "ISC",
8 | "private": false,
9 | "main_changeset": true,
10 | "exports": {
11 | "./package.json": "./package.json",
12 | ".": {
13 | "gradio": "./Index.svelte"
14 | }
15 | },
16 | "dependencies": {
17 | "@gradio/atoms": "0.11.0",
18 | "@gradio/icons": "0.8.1",
19 | "@gradio/statustracker": "0.9.4",
20 | "@gradio/utils": "0.7.0",
21 | "@xterm/xterm": "^5.5.0",
22 | "xterm-addon-fit": "^0.8.0"
23 | },
24 | "devDependencies": {
25 | "@gradio/preview": "^0.13.0",
26 | "@tsconfig/svelte": "^5.0.4"
27 | }
28 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `gradio_log`
2 |
3 |
4 | A Log component of Gradio which can easily continuously print any log file content into the user interface.
5 |
6 | > Credit: [Featurize 算力平台](https://featurize.cn)
7 |
8 | 
9 | 
10 |
11 | ## Installation
12 |
13 | ```bash
14 | pip install gradio_log
15 | ```
16 |
17 | ## Usage
18 |
19 | ```python
20 | import gradio as gr
21 | from gradio_log import Log
22 |
23 | log_file = "/path/to/your/log_file.txt"
24 |
25 | with gr.Blocks() as demo:
26 | Log(log_file, dark=True, xterm_font_size=12)
27 |
28 | if __name__ == "__main__":
29 | demo.launch()
30 | ```
31 |
32 | After launched, try to add some new logs into the log file, and you will see the new logs are printed in the user interface.
33 |
--------------------------------------------------------------------------------
/demo/app.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from pathlib import Path
3 |
4 | import gradio as gr
5 | from gradio_log import Log
6 |
7 |
8 | class CustomFormatter(logging.Formatter):
9 |
10 | green = "\x1b[32;20m"
11 | blue = "\x1b[34;20m"
12 | yellow = "\x1b[33;20m"
13 | red = "\x1b[31;20m"
14 | bold_red = "\x1b[31;1m"
15 | reset = "\x1b[0m"
16 | format = "%(asctime)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
17 |
18 | FORMATS = {
19 | logging.DEBUG: blue + format + reset,
20 | logging.INFO: green + format + reset,
21 | logging.WARNING: yellow + format + reset,
22 | logging.ERROR: red + format + reset,
23 | logging.CRITICAL: bold_red + format + reset,
24 | }
25 |
26 | def format(self, record):
27 | log_fmt = self.FORMATS.get(record.levelno)
28 | formatter = logging.Formatter(log_fmt)
29 | return formatter.format(record)
30 |
31 |
32 | formatter = CustomFormatter()
33 |
34 | log_file = "/tmp/gradio_log.txt"
35 | Path(log_file).touch()
36 |
37 | ch = logging.FileHandler(log_file)
38 | ch.setLevel(logging.DEBUG)
39 | ch.setFormatter(formatter)
40 |
41 | logger = logging.getLogger("gradio_log")
42 | logger.setLevel(logging.DEBUG)
43 | for handler in logger.handlers:
44 | logger.removeHandler(handler)
45 | logger.addHandler(ch)
46 |
47 |
48 | logger.info("The logs will be displayed in here.")
49 |
50 |
51 | def create_log_handler(level):
52 | def l(text):
53 | getattr(logger, level)(text)
54 |
55 | return l
56 |
57 |
58 | with gr.Blocks() as demo:
59 | text = gr.Textbox(label="Enter text to write to log file")
60 | with gr.Row():
61 | for l in ["debug", "info", "warning", "error", "critical"]:
62 | button = gr.Button(f"log as {l}")
63 | button.click(fn=create_log_handler(l), inputs=text)
64 | Log(log_file, dark=True)
65 |
66 |
67 | if __name__ == "__main__":
68 | demo.launch(ssr_mode=True)
69 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "hatchling",
4 | "hatch-requirements-txt",
5 | "hatch-fancy-pypi-readme>=22.5.0",
6 | ]
7 | build-backend = "hatchling.build"
8 |
9 | [project]
10 | name = "gradio_log"
11 | version = "0.0.8"
12 | description = "A Log component for Gradio which can easily show some log file in the interface."
13 | readme = "README.md"
14 | license = "Apache-2.0"
15 | requires-python = ">=3.8"
16 | authors = [{ name = "Chenglu", email = "chenglu.she@gmail.com" }]
17 | keywords = [
18 | "gradio-custom-component",
19 | "gradio-template-SimpleTextbox",
20 | "log",
21 | "gradio_log",
22 | "gradio_log_component",
23 | "gradio_print_log",
24 | ]
25 | # Add dependencies here
26 | dependencies = ["gradio>=4.0,<6.0"]
27 | classifiers = [
28 | 'Development Status :: 3 - Alpha',
29 | 'License :: OSI Approved :: Apache Software License',
30 | 'Operating System :: OS Independent',
31 | 'Programming Language :: Python :: 3',
32 | 'Programming Language :: Python :: 3 :: Only',
33 | 'Programming Language :: Python :: 3.8',
34 | 'Programming Language :: Python :: 3.9',
35 | 'Programming Language :: Python :: 3.10',
36 | 'Programming Language :: Python :: 3.11',
37 | 'Topic :: Scientific/Engineering',
38 | 'Topic :: Scientific/Engineering :: Artificial Intelligence',
39 | 'Topic :: Scientific/Engineering :: Visualization',
40 | ]
41 |
42 | [project.optional-dependencies]
43 | dev = ["build", "twine"]
44 |
45 | [tool.hatch.build]
46 | artifacts = ["/backend/gradio_log/templates", "*.pyi", "backend/gradio_log/templates", "backend/gradio_log/templates", "backend/gradio_log/templates", "backend/gradio_log/templates", "backend/gradio_log/templates", "backend/gradio_log/templates", "backend/gradio_log/templates", "backend/gradio_log/templates"]
47 |
48 | [tool.hatch.build.targets.wheel]
49 | packages = ["/backend/gradio_log"]
50 |
51 | [project.urls]
52 | Homepage = "https://github.com/louis-she/gradio-log"
53 | Repository = "https://github.com/louis-she/gradio-log"
54 |
--------------------------------------------------------------------------------
/demo/css.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: Inter;
3 | font-size: 16px;
4 | font-weight: 400;
5 | line-height: 1.5;
6 | -webkit-text-size-adjust: 100%;
7 | background: #fff;
8 | color: #323232;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | text-rendering: optimizeLegibility;
12 | }
13 |
14 | :root {
15 | --space: 1;
16 | --vspace: calc(var(--space) * 1rem);
17 | --vspace-0: calc(3 * var(--space) * 1rem);
18 | --vspace-1: calc(2 * var(--space) * 1rem);
19 | --vspace-2: calc(1.5 * var(--space) * 1rem);
20 | --vspace-3: calc(0.5 * var(--space) * 1rem);
21 | }
22 |
23 | .app {
24 | max-width: 748px !important;
25 | }
26 |
27 | .prose p {
28 | margin: var(--vspace) 0;
29 | line-height: var(--vspace * 2);
30 | font-size: 1rem;
31 | }
32 |
33 | code {
34 | font-family: "Inconsolata", sans-serif;
35 | font-size: 16px;
36 | }
37 |
38 | h1,
39 | h1 code {
40 | font-weight: 400;
41 | line-height: calc(2.5 / var(--space) * var(--vspace));
42 | }
43 |
44 | h1 code {
45 | background: none;
46 | border: none;
47 | letter-spacing: 0.05em;
48 | padding-bottom: 5px;
49 | position: relative;
50 | padding: 0;
51 | }
52 |
53 | h2 {
54 | margin: var(--vspace-1) 0 var(--vspace-2) 0;
55 | line-height: 1em;
56 | }
57 |
58 | h3,
59 | h3 code {
60 | margin: var(--vspace-1) 0 var(--vspace-2) 0;
61 | line-height: 1em;
62 | }
63 |
64 | h4,
65 | h5,
66 | h6 {
67 | margin: var(--vspace-3) 0 var(--vspace-3) 0;
68 | line-height: var(--vspace);
69 | }
70 |
71 | .bigtitle,
72 | h1,
73 | h1 code {
74 | font-size: calc(8px * 4.5);
75 | word-break: break-word;
76 | }
77 |
78 | .title,
79 | h2,
80 | h2 code {
81 | font-size: calc(8px * 3.375);
82 | font-weight: lighter;
83 | word-break: break-word;
84 | border: none;
85 | background: none;
86 | }
87 |
88 | .subheading1,
89 | h3,
90 | h3 code {
91 | font-size: calc(8px * 1.8);
92 | font-weight: 600;
93 | border: none;
94 | background: none;
95 | letter-spacing: 0.1em;
96 | text-transform: uppercase;
97 | }
98 |
99 | h2 code {
100 | padding: 0;
101 | position: relative;
102 | letter-spacing: 0.05em;
103 | }
104 |
105 | blockquote {
106 | font-size: calc(8px * 1.1667);
107 | font-style: italic;
108 | line-height: calc(1.1667 * var(--vspace));
109 | margin: var(--vspace-2) var(--vspace-2);
110 | }
111 |
112 | .subheading2,
113 | h4 {
114 | font-size: calc(8px * 1.4292);
115 | text-transform: uppercase;
116 | font-weight: 600;
117 | }
118 |
119 | .subheading3,
120 | h5 {
121 | font-size: calc(8px * 1.2917);
122 | line-height: calc(1.2917 * var(--vspace));
123 |
124 | font-weight: lighter;
125 | text-transform: uppercase;
126 | letter-spacing: 0.15em;
127 | }
128 |
129 | h6 {
130 | font-size: calc(8px * 1.1667);
131 | font-size: 1.1667em;
132 | font-weight: normal;
133 | font-style: italic;
134 | font-family: "le-monde-livre-classic-byol", serif !important;
135 | letter-spacing: 0px !important;
136 | }
137 |
138 | #start .md > *:first-child {
139 | margin-top: 0;
140 | }
141 |
142 | h2 + h3 {
143 | margin-top: 0;
144 | }
145 |
146 | .md hr {
147 | border: none;
148 | border-top: 1px solid var(--block-border-color);
149 | margin: var(--vspace-2) 0 var(--vspace-2) 0;
150 | }
151 | .prose ul {
152 | margin: var(--vspace-2) 0 var(--vspace-1) 0;
153 | }
154 |
155 | .gap {
156 | gap: 0;
157 | }
158 |
--------------------------------------------------------------------------------
/frontend/Index.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
141 |
142 |
152 | {#if loading_status}
153 |
158 | {/if}
159 |
160 |
{label}
161 |
162 |
163 |
164 |
165 |
171 |
--------------------------------------------------------------------------------
/backend/gradio_log/log.py:
--------------------------------------------------------------------------------
1 | """Log component of Gradio."""
2 |
3 | from __future__ import annotations
4 |
5 | from typing import Any, Literal, Optional
6 |
7 | import gradio as gr
8 | from gradio.components.base import FormComponent
9 | from gradio.events import Events
10 | from gradio_client.documentation import document
11 |
12 | FontWeight = Literal[
13 | "normal",
14 | "bold",
15 | "100",
16 | "200",
17 | "300",
18 | "400",
19 | "500",
20 | "600",
21 | "700",
22 | "800",
23 | "900",
24 | ]
25 |
26 | LogLevel = Literal["trace", "debug", "info", "warn", "error", "off"]
27 |
28 |
29 | @document()
30 | class Log(FormComponent):
31 | """
32 | Create a log component which can continuously read from a log file and display the content in a container.
33 | """
34 |
35 | EVENTS = [Events.load]
36 |
37 | def find_start_position(self) -> int:
38 | with open(self.log_file, "rb") as f:
39 | f.seek(0, 2)
40 |
41 | file_size = f.tell()
42 | lines_found = 0
43 | block_size = 1024
44 | blocks = []
45 |
46 | while f.tell() > 0 and lines_found <= self.tail:
47 | f.seek(max(f.tell() - block_size, 0))
48 | block = f.read(block_size)
49 | blocks.append(block)
50 | lines_found += block.count(b"\n")
51 | f.seek(-len(block), 1)
52 |
53 | all_read_bytes = b"".join(reversed(blocks))
54 | lines = all_read_bytes.splitlines()
55 |
56 | if self.tail >= len(lines):
57 | return 0
58 | last_lines = b"\n".join(lines[-self.tail :])
59 | return file_size - len(last_lines) - 1
60 |
61 | def get_current_reading_pos(self, session_hash: str) -> int:
62 | if session_hash not in self.current_reading_positions:
63 | self.current_reading_positions[session_hash] = self.find_start_position()
64 | return self.current_reading_positions[session_hash]
65 |
66 | def read_to_end(self, session_hash: str) -> bytes:
67 | with open(self.log_file, "rb") as f:
68 | current_pos = self.get_current_reading_pos(session_hash)
69 | f.seek(current_pos)
70 | b = f.read().decode()
71 | current_pos = f.tell()
72 | self.current_reading_positions[session_hash] = current_pos
73 | return b
74 |
75 | def __init__(
76 | self,
77 | log_file: str = None,
78 | tail: int = 100,
79 | dark: bool = False,
80 | height: str | int | None = 240,
81 | xterm_allow_proposed_api: Optional[bool] = False,
82 | xterm_allow_transparency: Optional[bool] = False,
83 | xterm_alt_click_moves_cursor: Optional[bool] = True,
84 | xterm_convert_eol: Optional[bool] = False,
85 | xterm_cursor_blink: Optional[bool] = False,
86 | xterm_cursor_inactive_style: Literal[
87 | "outline", "block", "bar", "underline", "none"
88 | ] = "outline",
89 | xterm_cursor_style: Literal["block", "underline", "bar"] = "block",
90 | xterm_cursor_width: Optional[int] = 1,
91 | xterm_custom_glyphs: Optional[bool] = False,
92 | xterm_disable_stdin: Optional[bool] = True,
93 | xterm_document_override: Optional[Any] = None,
94 | xterm_draw_bold_text_in_bright_colors: Optional[bool] = True,
95 | xterm_fast_scroll_modifier: Optional[
96 | Literal["none", "alt", "ctrl", "shift"]
97 | ] = "alt",
98 | xterm_fast_scroll_sensitivity: Optional[int] = 1,
99 | xterm_font_family: Optional[str] = "courier-new, courier, monospace",
100 | xterm_font_size: Optional[int] = 15,
101 | xterm_font_weight: Optional[FontWeight] = "normal",
102 | xterm_font_weight_bold: Optional[FontWeight] = "bold",
103 | xterm_ignore_bracketed_paste_mode: Optional[bool] = False,
104 | xterm_letter_spacing: Optional[float] = 0,
105 | xterm_line_height: Optional[float] = 1.0,
106 | xterm_log_level: Optional[LogLevel] = "info",
107 | xterm_mac_option_click_forces_selection: Optional[bool] = False,
108 | xterm_mac_option_is_meta: Optional[bool] = False,
109 | xterm_minimum_contrast_ratio: Optional[int] = 1,
110 | xterm_overview_ruler_width: Optional[int] = 0,
111 | xterm_rescale_overlapping_glyphs: Optional[bool] = False,
112 | xterm_screen_reader_mode: Optional[bool] = False,
113 | xterm_scroll_on_user_input: Optional[bool] = True,
114 | xterm_scroll_sensitivity: Optional[int] = 1,
115 | xterm_scrollback: Optional[int] = 1000,
116 | xterm_smooth_scroll_duration: Optional[int] = 0,
117 | xterm_tab_stop_width: Optional[int] = 8,
118 | xterm_windows_mode: Optional[bool] = False,
119 | *,
120 | label: str | None = None,
121 | info: str | None = None,
122 | every: float = 0.5,
123 | show_label: bool | None = None,
124 | container: bool = True,
125 | scale: int | None = None,
126 | min_width: int = 160,
127 | interactive: bool | None = None,
128 | visible: bool = True,
129 | elem_id: str | None = None,
130 | elem_classes: list[str] | str | None = None,
131 | render: bool = True,
132 | ):
133 | """
134 | For all the xterm options, please refer to the xterm.js documentation:
135 | https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/
136 |
137 | Parameters:
138 | log_file: the log file path to read from.
139 | tail: from the end of the file, the number of lines to start read from.
140 | dark: if True, will render the component in dark mode.
141 | label: 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.
142 | info: additional component description.
143 | every: New log pulling interval.
144 | show_label: if True, will display label.
145 | container: If True, will place the component in a container - providing some extra padding around the border.
146 | scale: 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.
147 | min_width: 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.
148 | interactive: if True, will be rendered as an editable textbox; if False, editing will be disabled. If not provided, this is inferred based on whether the component is used as an input or output.
149 | visible: If False, component will be hidden.
150 | elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
151 | elem_classes: 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.
152 | render: 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.
153 | """
154 | self.log_file = log_file
155 | self.tail = tail
156 | self.dark = dark
157 | self.current_pos = None
158 | self.height = height
159 | self.current_reading_positions = {}
160 |
161 | self.xterm_allow_proposed_api = xterm_allow_proposed_api
162 | self.xterm_allow_transparency = xterm_allow_transparency
163 | self.xterm_alt_click_moves_cursor = xterm_alt_click_moves_cursor
164 | self.xterm_convert_eol = xterm_convert_eol
165 | self.xterm_cursor_blink = xterm_cursor_blink
166 | self.xterm_cursor_inactive_style = xterm_cursor_inactive_style
167 | self.xterm_cursor_style = xterm_cursor_style
168 | self.xterm_cursor_width = xterm_cursor_width
169 | self.xterm_custom_glyphs = xterm_custom_glyphs
170 | self.xterm_disable_stdin = xterm_disable_stdin
171 | self.xterm_document_override = xterm_document_override
172 | self.xterm_draw_bold_text_in_bright_colors = (
173 | xterm_draw_bold_text_in_bright_colors
174 | )
175 | self.xterm_fast_scroll_modifier = xterm_fast_scroll_modifier
176 | self.xterm_fast_scroll_sensitivity = xterm_fast_scroll_sensitivity
177 | self.xterm_font_family = xterm_font_family
178 | self.xterm_font_size = xterm_font_size
179 | self.xterm_font_weight = xterm_font_weight
180 | self.xterm_font_weight_bold = xterm_font_weight_bold
181 | self.xterm_ignore_bracketed_paste_mode = xterm_ignore_bracketed_paste_mode
182 | self.xterm_letter_spacing = xterm_letter_spacing
183 | self.xterm_line_height = xterm_line_height
184 | self.xterm_log_level = xterm_log_level
185 | self.xterm_mac_option_click_forces_selection = (
186 | xterm_mac_option_click_forces_selection
187 | )
188 | self.xterm_mac_option_is_meta = xterm_mac_option_is_meta
189 | self.xterm_minimum_contrast_ratio = xterm_minimum_contrast_ratio
190 | self.xterm_overview_ruler_width = xterm_overview_ruler_width
191 | self.xterm_rescale_overlapping_glyphs = xterm_rescale_overlapping_glyphs
192 | self.xterm_screen_reader_mode = xterm_screen_reader_mode
193 | self.xterm_scroll_on_user_input = xterm_scroll_on_user_input
194 | self.xterm_scroll_sensitivity = xterm_scroll_sensitivity
195 | self.xterm_scrollback = xterm_scrollback
196 | self.xterm_smooth_scroll_duration = xterm_smooth_scroll_duration
197 | self.xterm_tab_stop_width = xterm_tab_stop_width
198 | self.xterm_windows_mode = xterm_windows_mode
199 |
200 | self.state = gr.State(None)
201 |
202 | super().__init__(
203 | label=label,
204 | info=info,
205 | every=every,
206 | show_label=show_label,
207 | container=container,
208 | scale=scale,
209 | min_width=min_width,
210 | interactive=interactive,
211 | visible=visible,
212 | elem_id=elem_id,
213 | elem_classes=elem_classes,
214 | render=render,
215 | inputs=[self.state],
216 | value=self.read_to_end,
217 | )
218 |
219 | self.load(self.handle_load_event, outputs=self.state)
220 |
221 | def handle_load_event(self, request: gr.Request) -> str:
222 | return request.session_hash
223 |
224 | def handle_unload_event(self, request: gr.Request):
225 | print("request on unload: ", request)
226 |
227 | def api_info(self) -> dict[str, Any]:
228 | return {"type": "string"}
229 |
230 | def example_payload(self) -> Any:
231 | pass
232 |
233 | def example_value(self) -> Any:
234 | pass
235 |
--------------------------------------------------------------------------------