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 NullHandler is attached to the wijjit_ssh logger at import time. logging.Logger.callHandlers() only falls back to lastResort when it finds zero handlers anywhere on the chain, so this single handler is what stops the stderr spray.

  • propagate is 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’s caplog working, which is why we need no equivalent of Wijjit’s wijjit_caplog workaround).

  • configure_logging() is opt-in. WijjitSSH.run() calls it because run() 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: object

Dispatches 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.opened

peer_ip

connection.rejected

peer_ip, reason

connection.closed

peer_ip

auth.ok

username, peer_ip, method

auth.failed

username, peer_ip, method

session.started

session_id, username, peer_ip

session.rejected

peer_ip, reason, and username when the refusal came after authentication

session.ended

session_id, username, peer_ip, reason, duration

session.rejected is emitted from two places and they do not carry the same fields: a session refused by max_sessions has authenticated, so it has a username, while one refused for requesting no pty is reported without one. Read the payload with fields.get(...) rather than subscripting it.

Note that session.ended fires for a session that never emitted session.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 an ended it never saw a started for.

Parameters:

on_event (callable, optional) – (event: str, fields: Mapping[str, object]) -> None. If None, every emit() 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.

Parameters:
  • event (str) – Event name, e.g. "session.ended".

  • **fields (object) – Event payload.

Return type:

None

wijjit_ssh.logging.EventHook

(event_name, fields) -> None.

Type:

Signature of the on_event metrics hook

alias of Callable[[str, Mapping[str, object]], None]

class wijjit_ssh.logging.SessionLog(logger, extra=None)[source]

Bases: LoggerAdapter

A 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 ContextVar for 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 in session_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, and peer_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:
Return type:

tuple[Any, MutableMapping[str, Any]]

wijjit_ssh.logging.configure_logging(destination=None, level=logging.INFO, *, format_string=None)[source]

Configure the wijjit_ssh logger tree.

Opt-in: this is never called on import. WijjitSSH.run() calls it (that entry point owns the process); WijjitSSH.start() and WijjitSSH.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/Path opens a UTF-8 file in append mode; a file object (e.g. sys.stderr) is wrapped in a StreamHandler; None (the default) silences the tree.

  • level (str or int, optional) – Level name ("DEBUG") or constant (logging.DEBUG). Default logging.INFO.

  • format_string (str, optional) – Custom Formatter format. Defaults to DEFAULT_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_ssh tree.

Parameters:

name (str) – Module name, typically __name__. Names that are not already under wijjit_ssh are prefixed with it.

Returns:

A logger guaranteed to be wijjit_ssh or a descendant of it.

Return type:

logging.Logger

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:

str

wijjit_ssh.logging.session_logger(session_id, username, peer_ip)[source]

Build a SessionLog for 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:

SessionLog