├── 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 | PyPI - Version 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 | ![static](./assets/static.png) 9 | ![dynamic](./assets/dynamic.gif) 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 | --------------------------------------------------------------------------------