Loggers

Logging and message display components for notebooks

Canvas and Logger

Unified logging across JavaScript and Python for Bridget monitoring

Bridget operates across multiple contexts: browser (JS), kernel (Python), and extensions. This module provides a unified logging system that works identically in all environments.

Architecture: - Canvas: Abstraction of a display area (where to write) - Logger: Logging interface (what/how to write)

Bridget loggers write to Bridget canvases, allowing you to monitor all Bridget activity in a single notebook cell - no need to check browser console or configure Python logging separately.

Used throughout Bridget for debugging Bridge operations, state updates, and plugin activity.

Canvas

Places to display content


source

Canvas

 Canvas ()

Base class for output display areas in notebooks

DHCanvas


source

DhCanvas

 DhCanvas (height:int=200)

Canvas using IPython DisplayHandle for dynamic updates

cnv = DhCanvas(height=200)
cnv.show("What's up, world!<br>")
cnv.add("Take me to your leader.<br>")
cnv.clear()
cnv.add('<b>There and Back Again</b><br>')
cnv.show()

There and Back Again
cnv.add('')
cnv.add('\n')
cnv.add('<br>')

FCanvas (kernel)

HTML element with a well-known id.

display(HTML(FCanvas_stl))
class FCanvas(Canvas, T.HasTraits):
    height = T.Int(200).tag(sync=True)
    elid = T.Unicode('').tag(sync=True)
    def show(self, content=None, **kwargs):
        prev_elid = self.elid
        elid = new_id('brd-logger-')
        s = content or ''
        display(HTML(
            f"<div id='{elid}' class='brd-logger' "
            f"style='width: 100%; max-height: {self.height}px;'>{(s+'<br>') if not prev_elid and s else ''}</div>"), 
            metadata=kwargs)
        time.sleep(0.25)
        self.elid = elid
        if prev_elid: 
            display(Javascript(f"""
debugger;
const prevEl = document.getElementById('{prev_elid}');
let prevHtml = prevEl?.innerHTML ?? '';
if (prevEl) prevEl.style.display = 'none';
if (prevEl) prevEl.innerHTML = '';
{self._el()}; if (el) {{ el.innerHTML = prevHtml + '{s.replace("'", "\\'")}' + '<br>';
el.scrollTop = el.scrollHeight;}}
"""))
    def hide(self): display(Javascript(f"{self._el()} el.style.display = 'none'"))
    def add(self, content, **kwargs):
        if content is not None: display(Javascript(self._js(str(content).replace("'", "\\'"))))
    def clear(self): display(Javascript(f"{self._el()} el.innerHTML = ''"))

    def _el(self): return f"const el=document.getElementById('{self.elid}')"
    def _js(self, s):
        return f"{self._el()}; if (el) {{el.innerHTML += '{s}'; el.scrollTop = el.scrollHeight;}}"
cnv = FCanvas(height=100)
cnv.show()
for c in 'abcdefgehijk': cnv.add(f"{c}<br>")
cnv.add(f'<span class="ts">1234567890</span> <span class="msg">msg</span><br>')
cnv.add("lorem ipsum dolor sit 'amet<br>")
cnv.show()
cnv.add({"a": 1})

This is obviously ugly and unwieldy. We need a more usable canvas, one that doen’t rely in IPython display system.. The only solution in modern Jupyter envs is a widget.

FCanvas (widget)

os.environ['DEBUG_BRIDGET'] = 'True'
fcanvas_esm = bundled(fcanvas_js)(debugger=DEBUG(), ts=True)

source

FCanvas

 FCanvas (height:int=200, elid:str='', **kwargs)

Main AnyWidget base class.

cleanupwidgets('cnv')

cnv = FCanvas.create(height=100, timeout=3)
test_eq(cnv.loaded(), True)
cnv.show('hello')
# test_eq(cnv.displayed(), True)
cnv.add(' bye<br>')
cnv.clear()
cnv.add('Goodbye to all that<br>')
cnv.add({'a': str(Path('a'))})  # convert to json, no '<br>'
cnv.add(f'<br><span class="ts">1234567890</span> <span class="msg">msg</span><br>')
cnv.hide()
cnv.add('hideous!<br>')
cnv.show()
cnv.close()
cleanupwidgets('cnv')

cnv = FCanvas.create(elid=FCanvas.new_elid())
test_eq(cnv.loaded(), True)
cnv.add('Hi & Bi<br>')
cnv.close()

NBLogger


source

NoopLogger

 NoopLogger ()

Logger that discards all messages


source

NBLogger

 NBLogger ()

Notebook loggers with show/hide/log capabilities

BasicLogger


source

BasicLogger

 BasicLogger (msg=None, canvas:__main__.Canvas|None=None, height:int=200,
              show:bool=True, history:bool=True, **kwargs)

Simple logger that displays messages in a scrollable div

bl = BasicLogger('BasicLogger initialized', height=100)
for i,x in enumerate(range(10)): bl.log(f'test{i}')
bl.msg(f'''<span style="color: red;">{'red '*100}</span>''')
bl.history()[-1]
'<span style="color: red;">red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red red </span>'

Wrong HTML: the message is shortened with the HTML format

bl.log('green '*100, fmt='<span style="color: green;">{s}</span><br>')
bl.show()
bl.active(False, 'BasicLogger disabled')
False
bl.error('test')
bl.show("Enabled")
Enabled
BasicLogger disabled
green green green green green green green green green green green green green green green green green green green green green green green gr…
red red red red red red red red red red red red red red red red red red red red red red red red red red red red re…
test9
test8
test7
test6
test5
test4
test3
test2
test1
test0
BasicLogger initialized
bl.close_canvas()
bl.show()
bl.log('closed')
test_eq(bl.active(), False)
This logger is closed.

FLogger


source

FLogger

 FLogger (msg=None, canvas:__main__.Canvas|None=None, height:int=200,
          show:bool=True, history:bool=True, **kwargs)

Simple logger that displays messages in a scrollable div

bl = FLogger('FLogger initialized', FCanvas.create(height=100, elid=FCanvas.new_elid(), timeout=2))
for i,x in enumerate(range(5)): bl.log(f'test{i}')
bl.error('red '*100, fmt=lambda s: f'<span style="color: red;">{s}</span>')
bl.show()
bl.active(False, 'FLogger disabled')
False
bl.log('test')
bl.close_canvas()
bl.log('closed')
bl.show()
test_eq(bl.active(), False)
This logger is closed.

Loguru logger (WIP)

class LoguruBasicLogger(BasicLogger):
    def __init__(self): 
        super().__init__()
        self._fmt = FC.noop
    
    def write(self, message: str) -> None:
        if rec := getattr(message, 'record', None):
            level = rec['level'].name
        else: 
            for level in level_colors: 
                if level in message: break
        # message = f"<span style='color: {level_colors[level]}'>{message}</span>"
        # self.msg(message, fmt=lambda s:f"<span style='color: {level_colors[level]}'>{s}</span>")
        self.msg(message, fmt=f"<span style='color: {level_colors[level]}'>""{s}</span><br>")
logger.remove()  # Remove default handler
handler_id = logger.add(
    (lbl := LoguruBasicLogger()).write, 
    format="{level} | {message}",  # Simple format, we'll add HTML in the sink
    colorize=False  # Disable ANSI colors
)
ERROR | This is an error
WARNING | This is a warning
INFO | This is an info message
DEBUG | This is a debug message
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning")
logger.error("This is an error")
class LoguruBasicLogger(BasicLogger):
    def __init__(self):
        super().__init__()
        self.fmt = FC.noop
    
    def _format(self, msg, fmt: Callable[[str], str]|str|None=None, truncate:bool=True, sep:str='') -> str:
        rec = getattr(msg, 'record', json.loads(msg))
        return (
            f"<div style='display: flex; gap: 8px'>"
            f"<span style='color: #888'>{rec['time'].strftime('%H:%M:%S')}</span>"
            f"<span style='color: {level_colors[rec['level'].name]}'>{rec['level'].name:8}</span>"
            f"<span>{shorten(rec['message'], mode='r', limit=self.max_len or 140)}</span>"
            f"</div>"
        )

    def write(self, message:str) -> None:
        self.msg(message, sep='')
        # self.msg(formatted_msg)
    

def configure_logger(basic_logger: BasicLogger) -> int:
    """Configure loguru to use a specific BasicLogger instance."""
    logger.remove()
    return logger.add(
        basic_logger.write,  # type: ignore
        serialize=True  # This makes loguru pass a json to write()
    )
lbl = LoguruBasicLogger()
handler_id = configure_logger(lbl)
15:13:32ERROR This is an error
15:13:32WARNING This is a warning
15:13:32INFO This is an info message
15:13:32DEBUG This is a debug message
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning")
logger.error("This is an error")
(my_logger := BasicLogger()).setup_loguru_sink(logger);  # type: ignore
15:13:33.608834error This is an error
15:13:33.608047warning This is a warning
15:13:33.607341info This is an info message
15:13:33.606536debug This is a debug message
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning")
logger.error("This is an error")
(my_flogger := FLogger()).setup_loguru_sink(logger);  # type: ignore
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning")
logger.error("This is an error")
my_flogger.close_canvas()