Skip to content

Commit

Permalink
Improve logging
Browse files Browse the repository at this point in the history
  • Loading branch information
Alan Fleming committed Nov 10, 2024
1 parent 78e85b9 commit e8a4d53
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 37 deletions.
15 changes: 5 additions & 10 deletions ipylab/jupyterfrontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from ipylab.dialog import Dialog
from ipylab.ipylab import IpylabBase, Readonly
from ipylab.launcher import Launcher
from ipylab.log import IpylabLogFormatter, IpylabLoggerAdapter, IpylabLogHandler, LogLevel
from ipylab.log import IpylabLogFormatter, IpylabLogHandler, LogLevel
from ipylab.menu import ContextMenu, MainMenu
from ipylab.notification import NotificationManager
from ipylab.sessions import SessionManager
Expand Down Expand Up @@ -71,7 +71,7 @@ class App(Ipylab):
sessions = Readonly(SessionManager)

console = Instance(ShellConnection, allow_none=True, read_only=True)
logging_handler = Instance(logging.Handler, allow_none=True, read_only=True)
logging_handler = Instance(IpylabLogHandler, read_only=True)

active_namespace = Unicode("", read_only=True, help="name of the current namespace")
selector = Unicode("", read_only=True, help="Selector class for context menus (css)")
Expand All @@ -92,10 +92,9 @@ def close(self):

@default("log")
def _default_log(self):
log = IpylabLoggerAdapter(logging.getLogger("ipylab"))
if isinstance(self.logging_handler, logging.Handler):
log.logger.addHandler(self.logging_handler)
return log
log = logging.getLogger("ipylab")
self.logging_handler.set_as_handler(log)
return logging.LoggerAdapter(log)

@default("logging_handler")
def _default_logging_handler(self):
Expand Down Expand Up @@ -218,10 +217,6 @@ async def _open_console():

return self.to_task(_open_console(), "Open console")

def toggle_log_console(self) -> Task[ShellConnection]:
# How can we check if the log console is open?
return self.commands.execute("logconsole:open", {"source": self.vpath})

def shutdown_kernel(self, vpath: str | None = None):
"Shutdown the kernel"
return self.operation("shutdownKernel", {"vpath": vpath})
Expand Down
9 changes: 6 additions & 3 deletions ipylab/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,18 @@ def launch_jupyterlab():

@hookimpl
def on_error(obj: Ipylab, source: ErrorSource, error: Exception):
msg = f"{source} {error}"
obj.log.exception(msg, extra={"source": source}, exc_info=error)
obj.log.exception(str(source), exc_info=error)
task = objects.get("error_task")
if isinstance(task, Task):
# Try to minimize the number of notifications.
if not task.done():
return
task.result().close()
a = NotifyAction(label="📝", caption="Toggle log console", callback=ipylab.app.toggle_log_console, keep_open=True)
msg = f"{ipylab.app} {source} {error} {obj=}"
if isinstance(obj, ipylab.ShellConnection):
a = NotifyAction(label="👀", caption="Activate", callback=obj.activate, keep_open=True)
else:
a = NotifyAction(label="📝", caption="Open console", callback=ipylab.app.open_console, keep_open=True)
objects["error_task"] = ipylab.app.notification.notify(msg, type=ipylab.NotificationType.error, actions=[a])


Expand Down
44 changes: 20 additions & 24 deletions ipylab/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class LogPayloadOutput(LogPayloadBase):


class IpylabLogHandler(logging.Handler):
loggers: ClassVar[weakref.WeakSet[logging.Logger]] = weakref.WeakSet()
_loggers: ClassVar[weakref.WeakSet[logging.Logger]] = weakref.WeakSet()

def __init__(self) -> None:
ipylab.app.observe(self._observe_app_log_level, "logger_level")
Expand All @@ -118,34 +118,30 @@ def __init__(self) -> None:
def _observe_app_log_level(self, change: dict):
level = LogLevel.to_numeric(change["new"])
self.setLevel(level)
for log in self.loggers:
for log in self._loggers:
log.setLevel(level)

def emit(self, record):
log = LogPayloadOutput(
type=LogTypes.output, level=LogLevel.to_level(record.levelno), data=self.parse_record(record)
)
ipylab.app.send_log_message(log)

def parse_record(self, record) -> OutputTypes:
msg = self.format(record)
if record.levelno <= log_name_mappings[LogLevel.warning]:
return OutputStream(output_type="stream", type="stdout", text=msg)
if record.exc_info and record.exc_info[2]:
(etype, value, tb) = record.exc_info
stb = ipylab.app._ipy_shell.InteractiveTB.structured_traceback(etype, value, tb) # type: ignore # noqa: SLF001
else:
value, stb = "", None
return OutputError(output_type="error", ename=str(value), evalue=msg, traceback=stb)

data = OutputStream(output_type="stream", type="stdout", text=msg)
log = LogPayloadOutput(type=LogTypes.output, level=LogLevel.to_level(record.levelno), data=data)
ipylab.app.send_log_message(log)

class IpylabLoggerAdapter(logging.LoggerAdapter):
def __init__(self, logger: logging.Logger, extra=None):
logger.setLevel(LogLevel.to_numeric(ipylab.app.logger_level))
IpylabLogHandler.loggers.add(logger)
super().__init__(logger, extra)
def set_as_handler(self, log: logging.Logger):
"Set this handler as a handler for log and keep the level in sync."
if log not in self._loggers:
log.addHandler(self)
log.setLevel(self.level)
self._loggers.add(log)


class IpylabLogFormatter(logging.Formatter):
def formatException(self, ei) -> str: # noqa: ARG002, N802
return ""
def formatException(self, ei) -> str: # noqa: N802
(etype, value, tb) = ei
sh = ipylab.app._ipy_shell # noqa: SLF001
if not sh:
return super().formatException(ei)
itb = sh.InteractiveTB
itb.verbose if ipylab.app.logging_handler.level <= log_name_mappings[LogLevel.debug] else itb.minimal
stb = itb.structured_traceback(etype, value, tb) # type: ignore
return itb.stb2text(stb)

0 comments on commit e8a4d53

Please sign in to comment.