Skip to content

Clausal Prolog — Structured Logging (log module)

Overview

The log module provides structured logging predicates backed by Python's logging module. It exposes logger creation, leveled log output, handler/formatter configuration, and level management as Clausal Prolog predicates.

Since Python's logging module is the backend, all of Python's handler ecosystem is available — file rotation, syslog, SMTP, JSON formatters, etc.

-import_from(log, [get_logger, info, debug, warning, error, set_level])

main(NAME) <- (
    get_logger("myapp", L),
    set_level(L, "debug"),
    debug(L, f"Starting with name={NAME}"),
    info(L, f"Hello, {NAME}!")
)

Or via module import:

-import_module(log)

main <- (
    log.get_logger("myapp", L),
    log.info(L, "ready")
)

Import

-import_from(log, [
    get_logger, debug, info, warning, error, critical,
    set_level, get_level, is_enabled_for, log,
    add_handler, remove_handler,
    stream_handler, file_handler, set_formatter,
    basic_config
])

The module name is log (not logging) to avoid shadowing Python's stdlib logging module.


log levels

Levels follow Python's standard hierarchy (ascending severity):

Level String Python constant
DEBUG "debug" logging.DEBUG (10)
INFO "info" logging.INFO (20)
WARNING "warning" (or "warn") logging.WARNING (30)
ERROR "error" logging.ERROR (40)
CRITICAL "critical" (or "fatal") logging.CRITICAL (50)

Level names are case-insensitive, written as strings (or atoms) when passed to predicates.


Logging predicates

All logging predicates always succeed — they are side-effects. A message below the logger's configured level is silently discarded (the predicate still succeeds).

debug/1, debug/2

debug(+Msg)
debug(+Logger, +Msg)

log Msg at DEBUG level. The arity-1 form uses the default "clausal" logger.

info/1, info/2

info(+Msg)
info(+Logger, +Msg)

log at INFO level.

warning/1, warning/2

warning(+Msg)
warning(+Logger, +Msg)

log at WARNING level.

error/1, error/2

error(+Msg)
error(+Logger, +Msg)

log at ERROR level.

critical/1, critical/2

critical(+Msg)
critical(+Logger, +Msg)

log at CRITICAL level.

log/3

log(+Logger, +Level, +Msg)

log at an arbitrary level. Level is a string ("debug", "info", etc.) or an integer.

Messages and f-strings

A message is a string or an atom (an f-string is a string, like "..."). The seam's f-string support means interpolation works naturally:

info(L, f"User {USERID} logged in from {IP}")

Logic variables in f-strings are auto-dereferenced at search time.


Logger management

get_logger/1, get_logger/2

get_logger(-Logger)
get_logger(+Name, -Logger)

Unify Logger with a Python logging.Logger instance. The arity-1 form returns the default "clausal" logger. Logger objects are opaque — they unify via identity, not structure.

Python's logger hierarchy applies: get_logger("myapp.db", L) creates a child of "myapp". Calling get_logger with the same name always returns the same logger instance.

set_level/2

set_level(+Logger, +Level)

Set the logger's level. Messages below this level will be discarded (but the logging predicate still succeeds). Level is a string or integer.

get_level/2

get_level(+Logger, -Level)

Unify Level with the logger's effective level name, a lowercase atom (e.g. debug, warning) -- the spelling set_level/2 takes. A bound Level may be the atom or the string, in either case ('WARNING', "warning"). A custom level name (logging.addLevelName(5, "TRACE")) is a lowercase atom too (trace); an unnamed numeric level ("Level 15") comes back as a string.

is_enabled_for/2

is_enabled_for(+Logger, +Level)

Succeeds if the logger would process a message at Level; fails otherwise. This is the one logging predicate that can fail — useful for guarding expensive message construction:

process(L, DATA) <- (
    ((is_enabled_for(L, "debug"), debug(L, f"Processing: {DATA}")) or True),
    do_work(DATA)
)

Handler management

stream_handler/2

stream_handler(+StreamName, -Handler)

Create a logging.StreamHandler. StreamName is "stdout" or "stderr".

file_handler/2

file_handler(+Path, -Handler)

Create a logging.FileHandler that writes to the given file path.

set_formatter/2

set_formatter(+Handler, +FormatString)

Set a logging.Formatter on the handler using Python's format string syntax (e.g. "%(asctime)s [%(levelname)s] %(message)s").

add_handler/2

add_handler(+Logger, +Handler)

Add a handler to the logger.

remove_handler/2

remove_handler(+Logger, +Handler)

Remove a handler from the logger.

basic_config/1

basic_config(+Opts)

Call logging.basicConfig() with a dict of options ({level: "info"}, keys declared or single-quoted). Supported keys: level, format, datefmt, filename, filemode, stream. Note: basicConfig only takes effect if the root logger has no handlers yet.


Examples

Basic usage

-import_from(log, [get_logger, info, warning, set_level])

init(L) <- (
    get_logger("myapp", L),
    set_level(L, "info"),
    info(L, "Application started")
)

process_item(L, ITEM) <- (
    ITEM > 0,
    info(L, f"Processing item {ITEM}")
)
process_item(L, ITEM) <- (
    ITEM =< 0,
    warning(L, f"Skipping invalid item {ITEM}")
)

Custom handler and formatter

-import_from(log, [
    get_logger, info, set_level,
    stream_handler, file_handler, set_formatter, add_handler
])

setup_logging(L) <- (
    get_logger("myapp", L),
    set_level(L, "debug"),
    file_handler("/var/log/myapp.log", FH),
    set_formatter(FH, "%(asctime)s [%(levelname)s] %(name)s: %(message)s"),
    add_handler(L, FH),
    stream_handler("stderr", SH),
    set_formatter(SH, "%(levelname)s: %(message)s"),
    add_handler(L, SH)
)

Logger hierarchy

-import_from(log, [get_logger, info, set_level])

Setup <- (
    get_logger("myapp", PARENT),
    set_level(PARENT, "info"),
    get_logger("myapp.db", DBLOG),
    set_level(DBLOG, "debug"),
    info(DBLOG, "DB logger inherits parent's handlers")
)

Implementation
  • Module: clausal/modules/py/logging.py
  • Predicates: ModulePredicate wrappers, the same pattern as the other clausal/modules/py/ modules
  • Backend: Python's logging module — all predicates delegate to logging.Logger methods
  • Tests: tests/test_logging_module.py, tests/fixtures/logging_basic.seam

Design decisions
  1. Logger objects are opaque Python values — passed around via unification, not inspectable as terms.
  2. Logging predicates always succeed — they are side effects. Level filtering happens inside Python's logging; the predicate succeeds regardless.
  3. is_enabled_for/2 is the exception — it succeeds or fails based on level, useful for guarding expensive message construction.
  4. Level names are strings — maps to Python constants internally. Both "warn"/"warning" and "fatal"/"critical" are accepted.
  5. f-string messages — no special formatting needed; the seam's f-string support handles interpolation with auto-deref of logic variables.
  6. Module name is log — avoids shadowing Python's logging stdlib module in the import machinery.

See also: I/O — write, writeln, and f-string output · Python Interop — ++() escape for custom logging handlers.