wijjit_ssh.logging
Logging for wijjit-ssh: the wijjit_ssh logger tree, session-bound logs,
and the metrics hook.
Why this module exists rather than reusing wijjit.logging_config.get_logger()
Wijjit’s get_logger roots a name under its own namespace by prefixing
"wijjit." - but only if not name.startswith("wijjit"). Our modules are
named wijjit_ssh.*, which does start with "wijjit", so the prefix is
never applied and the name passes through untouched. The result is a logger
called wijjit_ssh.server that is a sibling of the wijjit tree, not a
child of it.
That is not a cosmetic difference. wijjit.configure_logging() only clears,
handles, and sets propagate = False on the wijjit logger, so our loggers
inherit none of it - including configure_logging(None), the “turn logging
off” switch. A record from wijjit_ssh therefore propagates to the root
logger, and with no handler anywhere on the chain, logging.lastResort
prints it to stderr. In a process where a Wijjit TUI has imported
wijjit_ssh, a warning sprays across the alternate screen buffer and corrupts
the frame.
So wijjit_ssh owns its own tree. The library posture here is deliberately
the conventional one rather than Wijjit’s:
A
NullHandleris attached to thewijjit_sshlogger at import time.logging.Logger.callHandlers()only falls back tolastResortwhen it finds zero handlers anywhere on the chain, so this single handler is what stops the stderr spray.propagateis left True. A host application that configures the root logger receives our records, which is what a library embedded in someone else’s process should do (and it keeps pytest’scaplogworking, which is why we need no equivalent of Wijjit’swijjit_caplogworkaround).configure_logging()is opt-in.WijjitSSH.run()calls it becauserun()owns the process;start()/run_async()never do, because they may be one coroutine inside a larger application.
Note on the module name: this file is wijjit_ssh/logging.py, but import
logging below resolves to the standard library, not to itself - Python 3 uses
absolute imports. Importing this module as wijjit_ssh.logging is
unambiguous.
- wijjit_ssh.logging.LOGGER_NAME = 'wijjit_ssh'
Root of this package’s logger tree.
- class wijjit_ssh.logging.EventEmitter(on_event=None)[source]
Bases:
objectDispatches lifecycle events to a deployment’s optional metrics hook.
Lets a deployment wire up Prometheus (or anything else) without this package taking a dependency on a metrics library. Events emitted:
Event
Fields
connection.openedpeer_ipconnection.rejectedpeer_ip,reasonconnection.closedpeer_ipauth.okusername,peer_ip,methodauth.failedusername,peer_ip,methodsession.startedsession_id,username,peer_ipsession.rejectedpeer_ip,reason, andusernamewhen the refusal came after authenticationsession.endedsession_id,username,peer_ip,reason,durationsession.rejectedis emitted from two places and they do not carry the same fields: a session refused bymax_sessionshas authenticated, so it has ausername, while one refused for requesting no pty is reported without one. Read the payload withfields.get(...)rather than subscripting it.Note that
session.endedfires for a session that never emittedsession.started- a no-pty refusal, or an app factory that raised. A hook that pairs the two events (a gauge, a subscriber registry) has to tolerate anendedit never saw astartedfor.- Parameters:
on_event (callable, optional) –
(event: str, fields: Mapping[str, object]) -> None. If None, everyemit()is a no-op.
Examples
>>> from collections import Counter >>> counts: Counter[str] = Counter() >>> emitter = EventEmitter(lambda event, fields: counts.update([event])) >>> emitter.emit("session.started", session_id="3f9a1c04") >>> counts["session.started"] 1
- emit(event, /, **fields)[source]
Fire one event. Never raises.
A deployment’s counter must not be able to take down a session, so a raising hook is logged and swallowed: these callbacks run inside asyncssh callbacks, where an exception would propagate into the transport rather than anywhere useful.
- wijjit_ssh.logging.EventHook
(event_name, fields) -> None.- Type:
Signature of the
on_eventmetrics hook
- class wijjit_ssh.logging.SessionLog(logger, extra=None)[source]
Bases:
LoggerAdapterA logger bound to one SSH session, prefixing
[id user@ip].Wraps a plain logger so every record from a session carries the session id, username, and peer address without each call site having to pass them.
An adapter is used rather than a
ContextVarfor a structural reason: the asyncssh callbacks (session_started,data_received,connection_lost) run on asyncssh’s connection task, while the app runs in a task we create insession_started. A contextvar set inside the app task would be invisible from exactly the callbacks that need to log. Binding the context to the session object sidesteps the question of which task is running.- Parameters:
logger (logging.Logger) – The underlying logger to write through.
extra (dict) – Must contain
session_id,username, andpeer_ip.
Examples
>>> log = session_logger("3f9a1c04", "ada", "10.0.0.7") >>> log.info("pty requested") # -> "[3f9a1c04 ada@10.0.0.7] pty requested"
- process(msg, kwargs)[source]
Process the logging message and keyword arguments passed in to a logging call to insert contextual information. You can either manipulate the message itself, the keyword args or both. Return the message and kwargs modified (or not) to suit your needs.
Normally, you’ll only need to override this one method in a LoggerAdapter subclass for your specific needs.
- Parameters:
msg (Any)
kwargs (MutableMapping[str, Any])
- Return type:
tuple[Any, MutableMapping[str, Any]]
- wijjit_ssh.logging.configure_logging(destination=None, level=logging.INFO, *, format_string=None)[source]
Configure the
wijjit_sshlogger tree.Opt-in: this is never called on import.
WijjitSSH.run()calls it (that entry point owns the process);WijjitSSH.start()andWijjitSSH.run_async()do not, since they may be embedded in a host application that has its own logging setup.Unlike a Wijjit app - where stderr is the terminal the app is drawing on - an SSH server’s stdout/stderr are ordinary process streams, so stderr is a sane default destination and is what systemd/Docker expect to collect.
- Parameters:
destination (str, Path, file object, or None, optional) – Where records go. A
str/Pathopens a UTF-8 file in append mode; a file object (e.g.sys.stderr) is wrapped in aStreamHandler;None(the default) silences the tree.level (str or int, optional) – Level name (
"DEBUG") or constant (logging.DEBUG). Defaultlogging.INFO.format_string (str, optional) – Custom
Formatterformat. Defaults toDEFAULT_FORMAT.
- Return type:
None
Examples
>>> import sys >>> configure_logging(sys.stderr, level="DEBUG") >>> configure_logging("wijjit-ssh.log")
- wijjit_ssh.logging.get_logger(name)[source]
Return a logger rooted under the
wijjit_sshtree.- Parameters:
name (str) – Module name, typically
__name__. Names that are not already underwijjit_sshare prefixed with it.- Returns:
A logger guaranteed to be
wijjit_sshor a descendant of it.- Return type:
Examples
>>> logger = get_logger(__name__) >>> logger.debug("decoded %d bytes", 12)
- wijjit_ssh.logging.new_session_id()[source]
Return a short, unique-enough id for one session.
Eight hex characters: long enough that ids in a log file don’t collide in practice, short enough to sit in every line and still be greppable. This is a correlation handle, not a security token.
- Returns:
Eight lowercase hex characters, e.g.
"3f9a1c04".- Return type:
- wijjit_ssh.logging.session_logger(session_id, username, peer_ip)[source]
Build a
SessionLogfor one session.- Parameters:
session_id (str) – Correlation id, from
new_session_id().username (str) – Authenticated username.
peer_ip (str) – Client address.
- Returns:
A logger that prefixes every record with the session context.
- Return type: