logger

The logger package provides otto’s logging infrastructure including custom levels and formatters.

otto’s logger accessor.

'otto' is a plain logging.Logger (no subclass). otto modules emit via get_logger() / logging.getLogger(__name__); child loggers propagate to 'otto', where the CLI attaches handlers (see otto.logger.management). Configuring/replacing handlers is up to the application — otto-the-library only emits (and otto.logger attaches a NullHandler).

otto.logger.logger.get_logger(name: str | None = None) Logger

Return the 'otto' logger (or the 'otto.<name>' child).

Register short level-name aliases (WARN, CRIT) with the logging system.

Log formatters: multiline splitter and Rich-markup renderer.

otto.logger.formatters.format_log_time(dt: datetime) Text

Format a datetime as a bracketed [ YYYY-MM-DD HH:MM:SS.mmm ] Rich Text.

class otto.logger.formatters.MultilineFormatter(fmt=None, datefmt=None, style='%', validate=True, *, defaults=None)

Bases: Formatter

logging.Formatter that formats each line of a multiline message separately.

Prevents leading continuation lines from being emitted without the log prefix (timestamp/level), keeping log files and console output parseable.

format(record: LogRecord) str

Format the specified record as text.

The record’s attribute dictionary is used as the operand to a string formatting operation which yields the returned string. Before formatting the dictionary, a couple of preparatory steps are carried out. The message attribute of the record is computed using LogRecord.getMessage(). If the formatting string uses the time (as determined by a call to usesTime(), formatTime() is called to format the event time. If there is exception information, it is formatted using formatException() and appended to the message.

class otto.logger.formatters.RichFormatter(fmt: str = '{asctime} [{levelname:^7}] {message}', style: '%' | '{' = '{', **kwargs: Any)

Bases: MultilineFormatter

MultilineFormatter for the log file handler that controls Rich markup.

The console handler uses rich.logging.RichHandler directly; this formatter is attached to the file handler. When rich is True, markup is rendered to ANSI escape sequences via an internal capture console; when rich is False (the default), ANSI is stripped so log files stay plain.

format(record: LogRecord) str

Render Rich markup in the record, then delegate to MultilineFormatter.

property rich : bool

True when Rich ANSI output is enabled; False strips ANSI sequences.

Per-command / per-host logging disposition.

LogMode decides where a host’s command I/O is recorded — independent of the log level (INFO vs DEBUG), which stays native to the logger.info/ logger.debug call. See docs/superpowers/specs/2026-06-28-three-sink-logging-design.md.

class otto.logger.mode.LogMode(value)

Bases: Enum

Disposition of a host’s command echo/output across the log sinks.

  • NORMAL — logged at the call’s level, shown everywhere.

  • QUIET — suppressed from the console + console.log, kept in verbose.log.

  • NEVER — redacted from every sink at every level, including session diagnostics.

LogMode governs command I/O only; logger.warning/logger.error and other non-command records are never suppressed by it.

NORMAL = 'normal'
QUIET = 'quiet'
NEVER = 'never'
property rank : int

Restrictiveness rank, ascending NORMAL (0), QUIET (1), NEVER (2).

otto.logger.mode.effective_mode(*modes: LogMode) LogMode

Return the most restrictive of modes (NORMAL when called with none).

CLI-side logging management for otto.

otto-the-library only emits log records (via logging.getLogger('otto'…)) and never configures handlers — see otto.logger. This module is the application/CLI side: it configures the 'otto' logger’s handlers + formatters, creates each invocation’s output directory, and prunes old log directories.

Context-free by design (it does not import otto.context): create_output_dir creates and returns the per-run directory; the CLI records that path on OttoContext.output_dir. These functions are called only by otto/cli/*.py (and tests).

Scope guardrail: keep this strictly logging config + the per-run dir’s creation/rotation. It is not a catch-all.

otto.logger.management.verbose_floor(log_level: str) int

Return the ‘otto’ logger / verbose.log floor: DEBUG when debugging, else INFO.

otto.logger.management.set_capture_prefixes(prefixes: Iterable[str]) None

Stash the external-logger prefixes to attach once the QueueHandler exists.

Called from the CLI’s main callback (before any subcommand wires the listener via create_output_dir). The actual attach happens later in _add_log_handlers -> capture_external_loggers.

otto.logger.management.capture_external_loggers(prefixes: Iterable[str]) None

Route the named top-level loggers into otto’s sinks (CLI/app only).

Finds the shared QueueHandler on the 'otto' logger and attaches it to each prefix’s logger at the verbose floor, so product code using a plain logging.getLogger(__name__) is captured without third-party noise. A no-op when the QueueHandler does not exist yet (no output dir wired).

otto.logger.management.reset() None

Reset module state, detach otto’s handlers, and restore library-citizen state — test helper.

Unregisters atexit callbacks before stopping the listener so that real-exit teardown never double-stops a listener or prints a None output dir.

otto.logger.management.init_cli_logging(xdir: Path, log_level: str, keep_days: float, rich_log_file: bool = False, show_time: bool = False) None

Configure the 'otto' logger for a CLI invocation (was init_otto_logger).

otto.logger.management.create_output_dir(command: str, subcommand: str | None = None) Path

Create this invocation’s output dir, wire the file handler, prune old logs, and return it.

The caller records it on OttoContext.output_dir.

otto.logger.management.attach_console_suppress_filter(filt: Filter) None

Apply filt to the console + console.log handlers only (NOT verbose.log).

otto.logger.management.remove_old_logs(seconds: float, *, time_budget: float = 5.0) None

Remove log dirs older than seconds, time-boxed to time_budget.

When the budget is exceeded the scan stops early and resumes on the next call, bounding the per-run cost on large/slow (e.g. NFS) trees.