libsrg_log2web 1.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- libsrg_log2web/Demo.py +67 -0
- libsrg_log2web/FlaskApp.py +36 -0
- libsrg_log2web/Log2Web.py +293 -0
- libsrg_log2web/LoggerGUIProxy.py +77 -0
- libsrg_log2web/__init__.py +0 -0
- libsrg_log2web/static/style.css +8 -0
- libsrg_log2web/templates/status.html +30 -0
- libsrg_log2web-1.0.7.dist-info/METADATA +15 -0
- libsrg_log2web-1.0.7.dist-info/RECORD +11 -0
- libsrg_log2web-1.0.7.dist-info/WHEEL +5 -0
- libsrg_log2web-1.0.7.dist-info/top_level.txt +1 -0
libsrg_log2web/Demo.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from time import sleep
|
|
5
|
+
|
|
6
|
+
from libsrg.LoggingAppBase import LoggingAppBase
|
|
7
|
+
|
|
8
|
+
from libsrg_log2web import FlaskApp, Log2Web
|
|
9
|
+
from libsrg_log2web.LoggerGUIProxy import LoggerGUIProxy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Main(LoggingAppBase):
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
|
|
16
|
+
fmt = "%(asctime)s %(levelname)-8s %(lineno)4d %(name) 20s.%(funcName)-22s %(threadName)-12s-- %(message)s"
|
|
17
|
+
self.file0 = Path(__file__)
|
|
18
|
+
self.project_path = self.file0.parent
|
|
19
|
+
self.node = platform.node()
|
|
20
|
+
self.node0 = self.node.split(".")[0]
|
|
21
|
+
|
|
22
|
+
logfile_path = Path.home() / "Log2Web.log"
|
|
23
|
+
logfile_path.unlink(missing_ok=True)
|
|
24
|
+
self.logfile_name = str(logfile_path)
|
|
25
|
+
super().__init__(logfile=self.logfile_name, format=fmt)
|
|
26
|
+
|
|
27
|
+
self.bridge = Log2Web.Bridge(self.run, FlaskApp.app, title="Log2Web Demo",
|
|
28
|
+
headertext="Log2Web Demo Threads")
|
|
29
|
+
self.bridge.run()
|
|
30
|
+
|
|
31
|
+
def run(self) -> None:
|
|
32
|
+
self.logger.info("callback to application")
|
|
33
|
+
|
|
34
|
+
executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="POOL_")
|
|
35
|
+
[executor.submit(self.app_thread_target, i) for i in range(10)]
|
|
36
|
+
|
|
37
|
+
# sleep(10)
|
|
38
|
+
executor.shutdown(wait=True)
|
|
39
|
+
|
|
40
|
+
self.logger.debug("Bye")
|
|
41
|
+
# """Raises a signal to main thread which is WebGUI to shut down."""
|
|
42
|
+
# signal.raise_signal(2)
|
|
43
|
+
|
|
44
|
+
def app_thread_target(self, n: int) -> None:
|
|
45
|
+
|
|
46
|
+
LoggerGUIProxy.gui_new_line()
|
|
47
|
+
self.logger.info(f"app {n} starting")
|
|
48
|
+
for i in range(20):
|
|
49
|
+
match i:
|
|
50
|
+
case 3:
|
|
51
|
+
LoggerGUIProxy.gui_set_colors(foreground='pink', background='blue')
|
|
52
|
+
case 5:
|
|
53
|
+
LoggerGUIProxy.gui_set_colors(foreground='black', background='white')
|
|
54
|
+
case 7:
|
|
55
|
+
LoggerGUIProxy.gui_set_colors(foreground='cyan', background='red')
|
|
56
|
+
|
|
57
|
+
if i == n:
|
|
58
|
+
self.logger.warning(f"app {n=} cycle {i=}")
|
|
59
|
+
else:
|
|
60
|
+
self.logger.info(f"app {n=} cycle {i=}")
|
|
61
|
+
|
|
62
|
+
sleep(1 + n / 10.)
|
|
63
|
+
LoggerGUIProxy.gui_end_line()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
main = Main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from flask import Flask, render_template
|
|
4
|
+
from libsrg_log2web.Log2Web import LogWatcher, Bridge
|
|
5
|
+
|
|
6
|
+
my_file = __file__
|
|
7
|
+
flask_dir = Path(my_file).parent
|
|
8
|
+
app_dir = flask_dir.parent.parent
|
|
9
|
+
template_dir = flask_dir / "templates"
|
|
10
|
+
static_dir = flask_dir / "static"
|
|
11
|
+
app = Flask(__name__,
|
|
12
|
+
template_folder=template_dir,
|
|
13
|
+
static_folder=static_dir)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.route('/hello')
|
|
17
|
+
def hello_world(): # put the application's code here
|
|
18
|
+
return 'Hello World!'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.route('/')
|
|
22
|
+
def display_status(): # put the application's code here
|
|
23
|
+
# print("prepare display list")
|
|
24
|
+
watchers = LogWatcher.instance.prepare_display_list()
|
|
25
|
+
# print("rendering template")
|
|
26
|
+
page = render_template('status.html',
|
|
27
|
+
delay=0.25,
|
|
28
|
+
watchers=watchers,
|
|
29
|
+
title=Bridge.instance.title,
|
|
30
|
+
headertext=Bridge.instance.headertext)
|
|
31
|
+
# print("returning page")
|
|
32
|
+
return page
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == '__main__':
|
|
36
|
+
app.run()
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import signal
|
|
3
|
+
import threading
|
|
4
|
+
import webbrowser
|
|
5
|
+
from threading import Thread
|
|
6
|
+
from time import sleep, time
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
from flask import Flask
|
|
10
|
+
from libsrg_log2web.LoggerGUIProxy import LogOps, LOG_OP_FMT_STRING
|
|
11
|
+
"""
|
|
12
|
+
This module does not import the user's Flask application module or the user's Main module.
|
|
13
|
+
The user's Flask application can include the Log2Web module.
|
|
14
|
+
The user's Main module must import (and instantiate) the user's FlaskApp module.
|
|
15
|
+
|
|
16
|
+
The User's Main module must connect the FlaskApp module to the Log2Web module via a Bridge object.
|
|
17
|
+
|
|
18
|
+
As general guidance, attempting to log within the Log2Web module may cause infinite recursion.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
ThreadLogWatcher is an object that receives log messages from a given thread and retains information for display.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
status_color = {
|
|
31
|
+
logging.DEBUG: 'green',
|
|
32
|
+
logging.INFO: 'blue',
|
|
33
|
+
logging.WARNING: 'orange',
|
|
34
|
+
logging.ERROR: 'red',
|
|
35
|
+
logging.CRITICAL: 'magenta'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ThreadLogWatcher:
|
|
40
|
+
def __init__(self, record0: logging.LogRecord):
|
|
41
|
+
self.thread_id: int = record0.thread
|
|
42
|
+
self.live = True
|
|
43
|
+
self.time_of_death = None
|
|
44
|
+
self.record: logging.LogRecord = record0
|
|
45
|
+
self.fg = 'green'
|
|
46
|
+
self.bg = 'white'
|
|
47
|
+
self.worst = logging.DEBUG
|
|
48
|
+
self.latest = logging.DEBUG
|
|
49
|
+
self.tname = record0.threadName
|
|
50
|
+
self.msg = "No message yet"
|
|
51
|
+
self.args0 = record0.args
|
|
52
|
+
self.updated: bool = False
|
|
53
|
+
if len(self.args0) > 0 and isinstance(self.args0[0], LogOps):
|
|
54
|
+
print(f"created with OP {self.args0[0].name=} {type(self.args0[0])=}")
|
|
55
|
+
|
|
56
|
+
def process_log_record(self, record: logging.LogRecord):
|
|
57
|
+
# print(f"{self.thread_id} {record.getMessage()} {record.message=} {type(record.message)=} {record.args=}")
|
|
58
|
+
self.record = record
|
|
59
|
+
self.msg = record.msg
|
|
60
|
+
self.worst = max(self.worst, record.levelno)
|
|
61
|
+
self.latest = record.levelno
|
|
62
|
+
self.tname = record.threadName
|
|
63
|
+
self.updated = True
|
|
64
|
+
|
|
65
|
+
# noinspection PyUnreachableCode
|
|
66
|
+
def process_log_op(self, op: LogOps, record: logging.LogRecord, *args):
|
|
67
|
+
# print(f"PROC OP {self.thread_id} {op=} {type(op)=} {args=}")
|
|
68
|
+
self.record = record
|
|
69
|
+
match op:
|
|
70
|
+
case LogOps.FG_COLOR:
|
|
71
|
+
self.fg = args[1]
|
|
72
|
+
case LogOps.BG_COLOR:
|
|
73
|
+
self.bg = args[1]
|
|
74
|
+
case LogOps.NEW_THREAD:
|
|
75
|
+
print(f"NEW_THREAD thread {self.thread_id} got exit OP {op=} {args=}")
|
|
76
|
+
pass # no action, but expected
|
|
77
|
+
case LogOps.THREAD_EXIT:
|
|
78
|
+
self.live = False
|
|
79
|
+
self.time_of_death = time()
|
|
80
|
+
print(f"THREAD_EXIT {self.thread_id} got exit OP {op=} {args=}")
|
|
81
|
+
case _:
|
|
82
|
+
# noinspection PyUnreachableCode
|
|
83
|
+
print(f"Unknown OP {op=} {args=}")
|
|
84
|
+
pass
|
|
85
|
+
# print(f"PROC OP results {self.thread_id} {self.fg=} {self.bg=}")
|
|
86
|
+
|
|
87
|
+
# noinspection PyUnusedLocal
|
|
88
|
+
def terminated(self, op: LogOps, *args):
|
|
89
|
+
# print(f"TERMINATED {self.thread_id} OP {op.name=} {type(op)=}")
|
|
90
|
+
self.live = False
|
|
91
|
+
self.time_of_death = time()
|
|
92
|
+
|
|
93
|
+
def time_dead(self, now: float) -> float | None:
|
|
94
|
+
if self.live:
|
|
95
|
+
return 0
|
|
96
|
+
return now - self.time_of_death
|
|
97
|
+
|
|
98
|
+
def display_row_start(self) -> str:
|
|
99
|
+
"""HTML lead in"""
|
|
100
|
+
stat_w = status_color.get(self.worst, 'violet')
|
|
101
|
+
stat_l = status_color.get(self.latest, 'violet')
|
|
102
|
+
live_fg = 'green' if self.live else 'black'
|
|
103
|
+
live_bg = 'white' if self.live else 'lightgrey'
|
|
104
|
+
busy_fg = 'green' if self.updated else 'darkgrey'
|
|
105
|
+
self.updated = False
|
|
106
|
+
out = [
|
|
107
|
+
f"<td><span style='background-color:{live_bg};color:{live_fg};'>",
|
|
108
|
+
self.tname if self.live else f"<del>{self.tname}</del>",
|
|
109
|
+
f"</span></td>",
|
|
110
|
+
|
|
111
|
+
f"<td><span style='background-color:white;color:{stat_w};'>",
|
|
112
|
+
logging.getLevelName(self.worst),
|
|
113
|
+
f"</span></td>",
|
|
114
|
+
|
|
115
|
+
f"<td><div style='background-color:white;color:{stat_l};'>",
|
|
116
|
+
logging.getLevelName(self.latest),
|
|
117
|
+
f"</div></td>",
|
|
118
|
+
|
|
119
|
+
f"<td><div style='background-color:white;color:{busy_fg};'>",
|
|
120
|
+
f"{self.record.filename}:{self.record.lineno}@{self.record.funcName}",
|
|
121
|
+
f"</div></td>",
|
|
122
|
+
|
|
123
|
+
f"<td><div style='background-color:{self.bg};color:{self.fg};'>",
|
|
124
|
+
# f"{stat_w=} {stat_l=}<br>"
|
|
125
|
+
]
|
|
126
|
+
return '\n'.join(out)
|
|
127
|
+
|
|
128
|
+
def display_row_body(self) -> str:
|
|
129
|
+
return f"{str(self.msg)[:80]}"
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def display_row_end() -> str:
|
|
133
|
+
return "</div></td>"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
LogWatcher is a logging handler that can be attached to the logging subsystem to observe
|
|
138
|
+
log messages generated by all threads of the user's application.
|
|
139
|
+
It manages a collection of ThreadLogWatcher objects, one for each thread.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class LogWatcher(logging.Handler):
|
|
144
|
+
instance: LogWatcher = None
|
|
145
|
+
|
|
146
|
+
def __init__(self, *args, **kwargs):
|
|
147
|
+
"""
|
|
148
|
+
Constructor.
|
|
149
|
+
:param args: positional arguments passed to logging.Handler
|
|
150
|
+
:param kwargs: keyword arguments passed to logging.Handler
|
|
151
|
+
"""
|
|
152
|
+
super().__init__(*args, **kwargs)
|
|
153
|
+
self.kwargs = kwargs
|
|
154
|
+
logging.getLogger().addHandler(self)
|
|
155
|
+
LogWatcher.instance = self
|
|
156
|
+
self.thread_log_watchers: dict[int, ThreadLogWatcher] = {}
|
|
157
|
+
self.display_watchers: list[ThreadLogWatcher] = []
|
|
158
|
+
self.data_event = threading.Event()
|
|
159
|
+
self.mylock = threading.RLock()
|
|
160
|
+
self.listlock = threading.RLock()
|
|
161
|
+
|
|
162
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Emit a record. (record is recorded to queue)
|
|
165
|
+
:param record:
|
|
166
|
+
:return:
|
|
167
|
+
"""
|
|
168
|
+
if record.name == 'werkzeug':
|
|
169
|
+
return
|
|
170
|
+
with self.mylock:
|
|
171
|
+
self.data_event.set()
|
|
172
|
+
|
|
173
|
+
thread = record.thread
|
|
174
|
+
# tname = f"{record.funcName=} {record.threadName=} {record.name=} {record.filename=}"
|
|
175
|
+
watcher = self.thread_log_watchers.get(thread, None)
|
|
176
|
+
oflag = record.msg.startswith(LOG_OP_FMT_STRING) if isinstance(record.msg, str) else False
|
|
177
|
+
op = record.args[0] if (len(record.args) > 0) and oflag else None # and (record.message == LOGOP)
|
|
178
|
+
|
|
179
|
+
# print(f"EMIT {thread} {op=} {type(op)=} {record.getMessage()} {watcher=} {tname=} {oflag=}")
|
|
180
|
+
# On termination of a thread, remove the old watcher from the dictionary (but not the list of display watchers).
|
|
181
|
+
if watcher is not None and (op in [LogOps.NEW_THREAD, LogOps.THREAD_EXIT]):
|
|
182
|
+
self.thread_log_watchers.pop(thread)
|
|
183
|
+
watcher.terminated(op)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# dont create a watcher for EXIT ops
|
|
187
|
+
if op in [LogOps.NEW_THREAD, LogOps.THREAD_EXIT]:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# if there is no watcher for the thread, create one and add to dictionary and list of display watchers.
|
|
191
|
+
if watcher is None:
|
|
192
|
+
watcher = ThreadLogWatcher(record)
|
|
193
|
+
self.thread_log_watchers[thread] = watcher
|
|
194
|
+
self.display_watchers.append(watcher)
|
|
195
|
+
|
|
196
|
+
if op is None:
|
|
197
|
+
watcher.process_log_record(record)
|
|
198
|
+
else:
|
|
199
|
+
watcher.process_log_op(op, record, *record.args)
|
|
200
|
+
|
|
201
|
+
def prepare_display_list(self) -> list[ThreadLogWatcher]:
|
|
202
|
+
# wait up to 1 second for data to arrive.
|
|
203
|
+
# self.data_event.wait(timeout=0.5)
|
|
204
|
+
self.bring_out_your_dead()
|
|
205
|
+
watchers = self.display_watchers.copy()
|
|
206
|
+
if len(watchers) > 30:
|
|
207
|
+
watchers = [watcher for watcher in watchers if watcher.live]
|
|
208
|
+
return watchers
|
|
209
|
+
|
|
210
|
+
def bring_out_your_dead(self, delay: float = 5):
|
|
211
|
+
# print(f"Bringing out your dead before lock {delay=}")
|
|
212
|
+
with self.listlock:
|
|
213
|
+
# print(f"Bringing out your dead inside lock {delay=}")
|
|
214
|
+
self.data_event.clear()
|
|
215
|
+
|
|
216
|
+
now = time()
|
|
217
|
+
dead_watchers = list([watcher for watcher in self.display_watchers if watcher.time_dead(now) >= delay])
|
|
218
|
+
for watcher in dead_watchers:
|
|
219
|
+
self.display_watchers.remove(watcher)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
The bridge object connects the user application and flask application.
|
|
224
|
+
All three are expected to be singletons.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Bridge:
|
|
229
|
+
instance: Bridge = None
|
|
230
|
+
|
|
231
|
+
def __init__(self,
|
|
232
|
+
user_callable: Callable[[], None],
|
|
233
|
+
flask_app: Flask,
|
|
234
|
+
browser: bool = True,
|
|
235
|
+
title: str = "Log2Web",
|
|
236
|
+
headertext: str = "Data from threads",
|
|
237
|
+
address: str = "0.0.0.0",
|
|
238
|
+
port: int = 8080,
|
|
239
|
+
terminate_callback: Callable[[], None] = None,
|
|
240
|
+
) -> None:
|
|
241
|
+
self.user_callable = user_callable
|
|
242
|
+
self.address = address
|
|
243
|
+
self.port = port
|
|
244
|
+
self.flask_app = flask_app
|
|
245
|
+
self.user_thread = Thread(target=self._run_user_in_thread, name="USER_MAIN")
|
|
246
|
+
self.terminate_callback = terminate_callback
|
|
247
|
+
Bridge.instance = self
|
|
248
|
+
self.log_watcher = LogWatcher()
|
|
249
|
+
self.browser = browser
|
|
250
|
+
|
|
251
|
+
self.title = title
|
|
252
|
+
self.headertext = headertext
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
The run method swaps control of the main thread to the flask application.
|
|
256
|
+
The user application starts in the main thread. Flask must run in the main thread.
|
|
257
|
+
A second thread is created to run the user_callable provided in the constructor.
|
|
258
|
+
This method does not return until the flask application exits.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def run(self):
|
|
262
|
+
self.user_thread.start()
|
|
263
|
+
self.flask_app.run(host=self.address, port=self.port)
|
|
264
|
+
|
|
265
|
+
def _run_user_in_thread(self):
|
|
266
|
+
"""
|
|
267
|
+
Runs the user callable in a separate thread.
|
|
268
|
+
On completion, raises a signal to the main thread which is WebGUI to shut down.
|
|
269
|
+
:return:
|
|
270
|
+
"""
|
|
271
|
+
try:
|
|
272
|
+
if self.browser:
|
|
273
|
+
# noinspection HttpUrlsUsage
|
|
274
|
+
webbrowser.open(f"http://{self.address}:{self.port}")
|
|
275
|
+
logging.getLogger().info("starting user callable")
|
|
276
|
+
self.user_callable()
|
|
277
|
+
logging.getLogger().info("user callable completed")
|
|
278
|
+
sleep(4)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logging.getLogger().exception(e)
|
|
281
|
+
finally:
|
|
282
|
+
if self.terminate_callback is not None:
|
|
283
|
+
logging.getLogger().info("Calling terminate callback")
|
|
284
|
+
try:
|
|
285
|
+
self.terminate_callback()
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logging.getLogger().exception(e)
|
|
288
|
+
logging.getLogger().info("GUI Shutdown")
|
|
289
|
+
sleep(1)
|
|
290
|
+
signal.raise_signal(2)
|
|
291
|
+
|
|
292
|
+
if __name__ == "__main__":
|
|
293
|
+
print("This module does not run as a standalone program.")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# libsrg (Code and Documentation) is published under an MIT License
|
|
2
|
+
# Copyright (c) 2023,2024 Steven Goncalo
|
|
3
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
4
|
+
|
|
5
|
+
# 2024 Steven Goncalo
|
|
6
|
+
import logging
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LogOps(Enum):
|
|
11
|
+
NEW_THREAD = 1
|
|
12
|
+
THREAD_EXIT = 2
|
|
13
|
+
FG_COLOR = 3
|
|
14
|
+
BG_COLOR = 4
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
LOG_OP_FMT_STRING = "LOG_OP_FMT_STRING %s"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _send_log_op(op: LogOps, *args, logr=None):
|
|
21
|
+
fmt = LOG_OP_FMT_STRING
|
|
22
|
+
fmt += " %s" * len(args)
|
|
23
|
+
if not logr:
|
|
24
|
+
logr = logging.getLogger("LoggerGUIProxy")
|
|
25
|
+
# print(f"send op {op=} {args=} {fmt=}")
|
|
26
|
+
stacklevel = 2
|
|
27
|
+
logr.info(fmt, op, stacklevel=stacklevel, *args)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LoggerGUIProxy:
|
|
31
|
+
"""
|
|
32
|
+
LoggerGUIProxy provides several methods to control the LoggerGUI via calls to logging.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def gui_new_line(cls, logr=None):
|
|
37
|
+
"""
|
|
38
|
+
Schedule the GUI line item for this thread to be deleted and disassociate it with this thread.
|
|
39
|
+
If subsequent logging occurs from the same thread, a new GUI line is created.
|
|
40
|
+
"""
|
|
41
|
+
_send_log_op(LogOps.NEW_THREAD, logr=logr)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def gui_freeze_line(cls, logr=None):
|
|
45
|
+
"""
|
|
46
|
+
Freeze the current contents of the GUI line for this thread.
|
|
47
|
+
Do not schedule the GUI line item for this thread to be deleted but disassociate it with this thread.
|
|
48
|
+
If subsequent logging occurs from the same thread, a new GUI line is created.
|
|
49
|
+
"""
|
|
50
|
+
_send_log_op(LogOps.THREAD_EXIT, logr=logr)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def gui_end_line(cls, logr=None):
|
|
54
|
+
"""
|
|
55
|
+
Freeze the current contents of the GUI line for this thread.
|
|
56
|
+
Do not schedule the GUI line item for this thread to be deleted but disassociate it with this thread.
|
|
57
|
+
If subsequent logging occurs from the same thread, a new GUI line is created.
|
|
58
|
+
"""
|
|
59
|
+
_send_log_op(LogOps.THREAD_EXIT, logr=logr)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def gui_configure(cls, logr=None, **kwargs):
|
|
63
|
+
"""
|
|
64
|
+
Send one or more logs to set configuration values for the GUI line associated with this thread.
|
|
65
|
+
Each log sent is a single name/value pair.
|
|
66
|
+
"""
|
|
67
|
+
if "background" in kwargs:
|
|
68
|
+
_send_log_op(LogOps.BG_COLOR, kwargs["background"], logr=logr)
|
|
69
|
+
if "foreground" in kwargs:
|
|
70
|
+
_send_log_op(LogOps.FG_COLOR, kwargs["foreground"], logr=logr)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def gui_set_colors(cls, logr=None, foreground=None, background=None, ):
|
|
74
|
+
if foreground:
|
|
75
|
+
_send_log_op(LogOps.FG_COLOR, foreground, logr=logr)
|
|
76
|
+
if background:
|
|
77
|
+
_send_log_op(LogOps.BG_COLOR, background, logr=logr)
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta http-equiv="refresh" content="{{ delay }}">
|
|
6
|
+
<link href="../static/style.css" rel="stylesheet" type="text/css">
|
|
7
|
+
<title>{{title}}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
{{headertext}}
|
|
11
|
+
<table class="status">
|
|
12
|
+
<tr class="header">
|
|
13
|
+
<th>Thread</th>
|
|
14
|
+
<th>Net Status</th>
|
|
15
|
+
<th>Latest Status</th>
|
|
16
|
+
<th>Location</th>
|
|
17
|
+
<th>Message</th>
|
|
18
|
+
</tr>
|
|
19
|
+
{% for watcher in watchers %}
|
|
20
|
+
<tr>
|
|
21
|
+
{{ watcher.display_row_start()|safe }}
|
|
22
|
+
{{ watcher.display_row_body() }}
|
|
23
|
+
{{ watcher.display_row_end()|safe }}
|
|
24
|
+
|
|
25
|
+
</tr>
|
|
26
|
+
{% endfor %}
|
|
27
|
+
</table>
|
|
28
|
+
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: libsrg_log2web
|
|
3
|
+
Version: 1.0.7
|
|
4
|
+
Summary: Multithreded logging to webpage
|
|
5
|
+
Author-email: Steve Goncalo <steven@goncalo.us>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Topic :: Utilities
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
|
+
Requires-Dist: flask>=3.1.2
|
|
15
|
+
Requires-Dist: libsrg>=6.0.4
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
libsrg_log2web/Demo.py,sha256=bzPkIb-0mCuooME8q5pdUZzUn-Gc9_naMRo-8wdukBE,2224
|
|
2
|
+
libsrg_log2web/FlaskApp.py,sha256=x_xBZbRR5Al7hnqlp1cy5arC0Ba_3a-K8_eL56mx9OI,1027
|
|
3
|
+
libsrg_log2web/Log2Web.py,sha256=gOLqbDD103NNj3A_LpwKGEUb4fE3hrIv1F2sZrJTNaI,10837
|
|
4
|
+
libsrg_log2web/LoggerGUIProxy.py,sha256=_v7vScn1DEF9BT8XCuMTtQ_0u3yMjtmLlq7RiCOdI_k,2714
|
|
5
|
+
libsrg_log2web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
libsrg_log2web/static/style.css,sha256=GBktqyouX908PaeRx8BreKqrT9aAMEUQx08zSXV4UBU,106
|
|
7
|
+
libsrg_log2web/templates/status.html,sha256=ZM8a1x95JukuvGVMqp8IiBkwxXmzeLYGJXAZV42staQ,743
|
|
8
|
+
libsrg_log2web-1.0.7.dist-info/METADATA,sha256=u3b7zp8M0HAKBr2pd-i1ogWXmnGbNjaXI7_jsAkSA_U,521
|
|
9
|
+
libsrg_log2web-1.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
libsrg_log2web-1.0.7.dist-info/top_level.txt,sha256=1lhFTpg88gjbyp67E4jaEF4qOdgh4nS7XerT50wmelM,15
|
|
11
|
+
libsrg_log2web-1.0.7.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
libsrg_log2web
|