Skip to content

Commit d140cde

Browse files
committed
LIU-470: Per-app logging from the web UI
- Setup a logging handler for the AppDrop class - Add REST endpoint in the DropManagers to get collected logs for each AppDROP. - Add basic clickable link to REST API call in the DIM web UI
1 parent 3224f45 commit d140cde

File tree

8 files changed

+110
-11
lines changed

8 files changed

+110
-11
lines changed

daliuge-common/dlg/clients.py

+11
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ def graph_status(self, sessionId):
118118
)
119119
return ret
120120

121+
def get_drop_status(self, sid, did):
122+
ret = self._get_json(f"/sessions/{quote(sid)}/graph/drop/{quote(did)}")
123+
logger.debug(
124+
"Successfully read graph status from session %s on %s:%s",
125+
sid,
126+
self.host,
127+
self.port,
128+
)
129+
return ret
130+
121131
def graph(self, sessionId):
122132
"""
123133
Returns a dictionary where the key are the DROP UIDs, and the values are
@@ -220,6 +230,7 @@ def graph_size(self, sessionId):
220230
getGraphStatus = graph_status
221231
getGraphSize = graph_size
222232
getGraph = graph
233+
getDropStatus = get_drop_status
223234

224235

225236
class NodeManagerClient(BaseDROPManagerClient):

daliuge-engine/dlg/apps/app_base.py

+40-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from collections import OrderedDict
33
from concurrent.futures import Future
44
from typing import List, Callable
5+
import json
56
import logging
67
import math
78
import threading
@@ -73,7 +74,25 @@ def thread_target():
7374

7475
_SYNC_DROP_RUNNER = SyncDropRunner()
7576

76-
77+
class InstanceLogHandler(logging.Handler):
78+
"""Custom handler to store logs in-memory per object instance."""
79+
def __init__(self, log_storage):
80+
super().__init__()
81+
self.log_storage = log_storage
82+
83+
def emit(self, record):
84+
"""Store log messages in the instance's log storage."""
85+
log_entry = self.format(record)
86+
self.log_storage.append(log_entry)
87+
88+
class AppLogFilter(logging.Filter):
89+
def __init__(self, uid: str, humanKey: str):
90+
self.uid = uid
91+
self.humanKey = humanKey
92+
93+
def filter(self, record):
94+
uid = getattr(record, "drop_uid", None)
95+
return uid == self.uid or uid == self.humanKey
7796
# ===============================================================================
7897
# AppDROP classes follow
7998
# ===============================================================================
@@ -141,6 +160,18 @@ def initialize(self, **kwargs):
141160
# by default run drops synchronously
142161
self._drop_runner: DropRunner = _SYNC_DROP_RUNNER
143162

163+
self.log_storage = []
164+
165+
self.logger = logging.getLogger(f"{__class__}.{self.uid}")
166+
instance_handler = InstanceLogHandler(self.log_storage)
167+
instance_handler.addFilter(AppLogFilter(self.uid, self._humanKey))
168+
169+
# Attach instance-specific handler
170+
logging.root.addHandler(instance_handler)
171+
172+
# Ensure logs still propagate to the root logger
173+
logger.propagate = True
174+
144175
@track_current_drop
145176
def addInput(self, inputDrop, back=True):
146177
uid = inputDrop.uid
@@ -308,6 +339,14 @@ def skip(self):
308339
execStatus=self.execStatus,
309340
)
310341

342+
def getLogs(self):
343+
"""
344+
:return: Return the logs stored in the logging handler
345+
"""
346+
347+
return self.log_storage
348+
349+
311350

312351
class InputFiredAppDROP(AppDROP):
313352
"""

daliuge-engine/dlg/manager/composite_manager.py

+30-4
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ def getPastSessionIds(self) -> list[str]:
306306
for path in self._past_session_manager.past_sessions(self._sessionIds)
307307
]
308308

309-
def _do_in_host(self, action, sessionId, exceptions, f, collect, port, iterable):
309+
def _do_in_host(self, action, sessionId, exceptions, f, collect, port, iterable, **kwargs):
310310
"""
311311
Replication of commands to underlying drop managers
312312
If "collect" is given, then individual results are also kept in the given
@@ -324,7 +324,7 @@ def _do_in_host(self, action, sessionId, exceptions, f, collect, port, iterable)
324324

325325
try:
326326
with self.dmAt(host) as dm:
327-
res = f(dm, iterable, sessionId)
327+
res = f(dm, iterable, sessionId, **kwargs)
328328

329329
if isinstance(collect, dict):
330330
collect.update(res)
@@ -342,7 +342,8 @@ def _do_in_host(self, action, sessionId, exceptions, f, collect, port, iterable)
342342
f,
343343
)
344344

345-
def replicate(self, sessionId, f, action, collect=None, iterable=None, port=None):
345+
def replicate(self, sessionId, f, action, collect=None, iterable=None, port=None,
346+
**kwargs):
346347
"""
347348
Replicates the given function call on each of the underlying drop managers
348349
"""
@@ -352,7 +353,8 @@ def replicate(self, sessionId, f, action, collect=None, iterable=None, port=None
352353
logger.debug("Replicating command: %s on hosts: %s", f, iterable)
353354
self._tp.map(
354355
functools.partial(
355-
self._do_in_host, action, sessionId, thrExs, f, collect, port
356+
self._do_in_host, action, sessionId, thrExs, f, collect, port,
357+
**kwargs
356358
),
357359
iterable,
358360
)
@@ -598,6 +600,30 @@ def getGraphStatus(self, sessionId):
598600
)
599601
return allStatus
600602

603+
def getDropStatus(self, sessionId, dropId):
604+
allstatus = {}
605+
self.replicate(
606+
sessionId,
607+
# {"session": sessionId, "drop": dropId},
608+
self._getDropStatus,
609+
"getting graph status",
610+
collect=allstatus,
611+
dropId=dropId
612+
)
613+
return allstatus
614+
615+
def _getDropStatus(self, dm, host, sessionId, dropId ):
616+
"""
617+
See session.getDropLogs()
618+
619+
:param dm:
620+
:param host:
621+
:param sessionId:
622+
:param dropId:
623+
:return: JSON of status logs and DROP information
624+
"""
625+
return dm.getDropStatus(sessionId, dropId)
626+
601627
def _getGraph(self, dm, host, sessionId):
602628
return dm.getGraph(sessionId)
603629

daliuge-engine/dlg/manager/node_manager.py

+3
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,9 @@ def getGraphStatus(self, sessionId):
364364
self._check_session_id(sessionId)
365365
return self._sessions[sessionId].getGraphStatus()
366366

367+
def getDropStatus(self, sessionId, dropId):
368+
return self._sessions[sessionId].getDropLogs(dropId)
369+
367370
def getGraph(self, sessionId):
368371
self._check_session_id(sessionId)
369372
# TODO: Ensure returns reproducibility data.

daliuge-engine/dlg/manager/rest.py

+5
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ def __init__(self, dm, maxreqsize=10):
205205
method="OPTIONS",
206206
callback=self.acceptPreflight2,
207207
)
208+
app.get("/api/sessions/<sessionId>/graph/drop/<dropId>", callback=self.getDropStatus)
208209

209210
# The non-REST mappings that serve HTML-related content
210211
app.route("/static/<filepath:path>", callback=self.server_static)
@@ -334,6 +335,10 @@ def getGraphSize(self, sessionId):
334335
def getGraphStatus(self, sessionId):
335336
return self.dm.getGraphStatus(sessionId)
336337

338+
@daliuge_aware
339+
def getDropStatus(self, sessionId, dropId):
340+
return self.dm.getDropStatus(sessionId, dropId)
341+
337342
@daliuge_aware
338343
def addGraphSpec(self, sessionId):
339344
# WARNING: TODO: Somehow, the content_type can be overwritten to 'text/plain'

daliuge-engine/dlg/manager/session.py

+12
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,18 @@ def getGraphStatus(self):
642642

643643
return statusDict
644644

645+
def getDropLogs(self, drop_oid: str):
646+
"""
647+
Retrieve the logs stored in the given DROP
648+
:param drop_oid: drop_oid
649+
650+
:return:
651+
"""
652+
return {"session": self.sessionId,
653+
"status": self.status,
654+
"logs": self._drops[drop_oid].getLogs() }
655+
656+
645657
@track_current_session
646658
def cancel(self):
647659
status = self.status

daliuge-engine/dlg/manager/web/session.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
d3.select('#pg-graph').remove();
9696

9797
const width = $('#pg-progress-bar').innerWidth();
98-
var graph_update_handler = function(oids, dropSpecs) {};
98+
var graph_update_handler = function(oids, dropSpecs, url) {};
9999

100100
var status_update_handler = function(statuses){
101101
// This is the order in which blocks are drawn in the progress bar,

daliuge-engine/dlg/manager/web/static/js/dm.js

+8-5
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ function promptNewSession(serverUrl, tbodyEl, refreshBtn) {
457457
});
458458
}
459459

460-
function drawGraphForDrops(g, drawGraph, oids, doSpecs) {
460+
function drawGraphForDrops(g, drawGraph, oids, doSpecs, url) {
461461

462462
// Keep track of modifications to see if we need to re-draw
463463
var modified = false;
@@ -468,7 +468,7 @@ function drawGraphForDrops(g, drawGraph, oids, doSpecs) {
468468
for (var idx in oids) {
469469
if (oids[idx] != 'reprodata') {
470470
var doSpec = doSpecs[oids[idx]];
471-
modified |= _addNode(g, doSpec);
471+
modified |= _addNode(g, doSpec, url);
472472
}
473473
}
474474

@@ -572,7 +572,7 @@ function startStatusQuery(serverUrl, sessionId, selectedNode, graph_update_handl
572572
if (oids.length > 0) {
573573
// Get sorted oids
574574
oids.sort();
575-
graph_update_handler(oids, doSpecs);
575+
graph_update_handler(oids, doSpecs, url);
576576
}
577577

578578
// During PRISITINE and BUILDING we need to update the graph structure
@@ -603,7 +603,7 @@ function startStatusQuery(serverUrl, sessionId, selectedNode, graph_update_handl
603603
var updateGraphTimer = d3.timer(updateGraph);
604604
}
605605

606-
function _addNode(g, doSpec) {
606+
function _addNode(g, doSpec, url) {
607607

608608
if (g.hasNode(g)) {
609609
return false;
@@ -626,13 +626,16 @@ function _addNode(g, doSpec) {
626626
else if (doSpec.type == 'plain') {
627627
notes += 'storage: ' + doSpec.storage;
628628
}
629-
629+
url = url + "/graph/drop/" + doSpec.oid;
630+
// let link = "<a href=" + url + target='_blank'>Click Me</a>";
631+
let link = "<a href=" + url + " target='_blank'>Click Me</a>";
630632
var oid = doSpec.oid;
631633
var html = '<div class="drop-label ' + typeShape + '" id="id_' + oid + '">';
632634
html += '<span class="notes">' + notes + '</span>';
633635
oid_date = doSpec.oid.split("_")[0];
634636
human_readable_id = oid_date + "_" + doSpec.humanReadableKey.toString()
635637
html += '<span style="font-size: 13px;">' + human_readable_id + '</span>';
638+
html += '<span style="font-size: 13px;">' + link + '</span>';
636639
html += "</div>";
637640
g.setNode(oid, {
638641
labelType: "html",

0 commit comments

Comments
 (0)