Skip to content

woodglue.log_setup

Logging setup for wgl start: journal, rotated file, or a logging: dictConfig.

Logging setup for wgl start.

Woodglue configures the root logger itself from woodglue.yaml; lythonic only contributes NodeRunLogFilter, which adds run_id and node_label to records logged inside a node run.

Default behavior

Under systemd (detected by JOURNAL_STREAM in the environment), records go to stderr without a timestamp, since the journal adds its own. Every line carries an sd-daemon priority prefix (<7> DEBUG, <6> INFO, <4> WARNING, <3> ERROR, <2> CRITICAL) so journald records the right priority. No file log is written unless storage.log_file is set explicitly.

Outside systemd, records go to <data>/wgl.log (or storage.log_file).

Every file log is a RotatingFileHandler, rotated at storage.log_max_bytes (default 10 MB) keeping storage.log_backup_count backups (default 5).

storage:
  log_file: logs/wgl.log   # relative to the data dir; null disables the file log
  log_level: INFO          # root level, INFO when unset
  log_max_bytes: 10485760
  log_backup_count: 5
  loggers:                 # per-logger levels
    tornado.access: WARNING

With log_file: null outside systemd, records go to stderr with a timestamp so they are not lost. An unknown level name in log_level or loggers stops wgl start with a ValueError rather than silently falling back to DEBUG.

Custom setup with logging:

A top-level logging: section in woodglue.yaml is passed to logging.config.dictConfig and replaces everything above, including the storage.log_* settings. Attach NodeRunLogFilter to keep the run context:

logging:
  version: 1
  disable_existing_loggers: false
  filters:
    noderun:
      (): lythonic.compose.log_context.NodeRunLogFilter
  formatters:
    plain:
      format: "%(asctime)s %(levelname)s [%(name)s] run=%(run_id)s %(message)s"
  handlers:
    console:
      class: logging.StreamHandler
      formatter: plain
      filters: [noderun]
  root:
    level: INFO
    handlers: [console]

Keep disable_existing_loggers: false: the dictConfig default of true silences every logger created before setup, including woodglue's own.

JournalFormatter

Bases: Formatter

Prefix every output line with the sd-daemon priority of the record's level.

journald reads the prefix per line, so the continuation lines of a traceback keep the record's priority instead of defaulting to INFO.

Source code in src/woodglue/log_setup.py
class JournalFormatter(logging.Formatter):
    """
    Prefix every output line with the sd-daemon priority of the record's level.

    journald reads the prefix per line, so the continuation lines of a traceback
    keep the record's priority instead of defaulting to INFO.
    """

    @override
    def format(self, record: logging.LogRecord) -> str:
        prefix = f"<{_sd_priority(record.levelno)}>"
        return "\n".join(prefix + line for line in super().format(record).split("\n"))

setup_logging(config, env=None)

Configure the root logger for wgl start and return a one-line description of what is in effect, e.g. Logging to journal.

Expects config.storage already resolved against the data dir. Safe to call repeatedly: handlers from a previous call are replaced, not duplicated. env defaults to os.environ.

Source code in src/woodglue/log_setup.py
def setup_logging(config: WoodglueConfig, env: Mapping[str, str] | None = None) -> str:
    """
    Configure the root logger for `wgl start` and return a one-line description
    of what is in effect, e.g. `Logging to journal`.

    Expects `config.storage` already resolved against the data dir. Safe to call
    repeatedly: handlers from a previous call are replaced, not duplicated.
    `env` defaults to `os.environ`.
    """
    env = os.environ if env is None else env
    root = logging.getLogger()
    _remove_installed_handlers(root)

    if config.logging is not None:
        logging.config.dictConfig(config.logging)
        return "Logging per woodglue.yaml logging: section"

    storage = config.storage
    under_systemd = "JOURNAL_STREAM" in env
    log_file: Path | None = storage.log_file
    if under_systemd and not storage.log_file_explicit:
        log_file = None

    targets: list[str] = []
    if under_systemd:
        _add_handler(
            root, "journal", logging.StreamHandler(sys.stderr), JournalFormatter(_RECORD_FORMAT)
        )
        targets.append("journal")
    if log_file is not None:
        log_file.parent.mkdir(parents=True, exist_ok=True)
        file_handler = logging.handlers.RotatingFileHandler(
            log_file,
            maxBytes=storage.log_max_bytes,
            backupCount=storage.log_backup_count,
            encoding="utf-8",
        )
        _add_handler(root, "file", file_handler, logging.Formatter(_TIMESTAMPED_FORMAT))
        targets.append(str(log_file))
    if not targets:
        _add_handler(
            root,
            "stderr",
            logging.StreamHandler(sys.stderr),
            logging.Formatter(_TIMESTAMPED_FORMAT),
        )
        targets.append("stderr")

    root.setLevel(storage.log_level.upper())
    for logger_name, level_name in storage.loggers.items():
        logging.getLogger(logger_name).setLevel(level_name.upper())

    return "Logging to " + " and ".join(targets)