Skip to content

Clausal — 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 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, [GetLogger, Info, Debug, Warning, Error, SetLevel])

Main(NAME) <- (
    GetLogger("myapp", L),
    SetLevel(L, "debug"),
    Debug(L, f"Starting with name={NAME}"),
    Info(L, f"Hello, {NAME}!")
)

Or via module import:

-import_module(log)

Main <- (
    log.GetLogger("myapp", L),
    log.Info(L, "ready")
)

Import

-import_from(log, [
    GetLogger, Debug, Info, Warning, Error, Critical,
    SetLevel, GetLevel, IsEnabledFor, Log,
    AddHandler, RemoveHandler,
    StreamHandler, FileHandler, SetFormatter,
    BasicConfig
])

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 strings 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

# skip
Debug(+Msg)
Debug(+Logger, +Msg)

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

Info/1, Info/2

# skip
Info(+Msg)
Info(+Logger, +Msg)

Log at INFO level.

Warning/1, Warning/2

# skip
Warning(+Msg)
Warning(+Logger, +Msg)

Log at WARNING level.

Error/1, Error/2

# skip
Error(+Msg)
Error(+Logger, +Msg)

Log at ERROR level.

Critical/1, Critical/2

# skip
Critical(+Msg)
Critical(+Logger, +Msg)

Log at CRITICAL level.

Log/3

# skip
Log(+Logger, +Level, +Msg)

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

Messages and f-strings

Messages are Python strings. Clausal's f-string support means interpolation works naturally:

# skip
Info(L, f"User {USERID} logged in from {IP}")

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


Logger management

GetLogger/1, GetLogger/2

# skip
GetLogger(-Logger)
GetLogger(+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: GetLogger("myapp.db", L) creates a child of "myapp". Calling GetLogger with the same name always returns the same logger instance.

SetLevel/2

# skip
SetLevel(+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.

GetLevel/2

# skip
GetLevel(+Logger, -Level)

Unify Level with the logger's effective level name (e.g. "DEBUG", "WARNING").

IsEnabledFor/2

# skip
IsEnabledFor(+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) <- (
    (IsEnabledFor(L, "debug"), Debug(L, f"Processing: {DATA}") or True),
    do_work(DATA)
)

Handler management

StreamHandler/2

# skip
StreamHandler(+StreamName, -Handler)

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

FileHandler/2

# skip
FileHandler(+Path, -Handler)

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

SetFormatter/2

# skip
SetFormatter(+Handler, +FormatString)

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

AddHandler/2

# skip
AddHandler(+Logger, +Handler)

Add a handler to the logger.

RemoveHandler/2

# skip
RemoveHandler(+Logger, +Handler)

Remove a handler from the logger.

BasicConfig/1

# skip
BasicConfig(+Opts)

Call logging.basicConfig() with a Python dict of options. Supported keys: level, format, datefmt, filename, filemode, stream. Note: basicConfig only takes effect if the root logger has no handlers yet.


Examples

Basic usage

```clausal

skip

-import_from(log, [GetLogger, Info, Warning, SetLevel])

Init(L) <- (
    GetLogger("myapp", L),
    SetLevel(L, "info"),
    Info(L, "Application started")
)

ProcessItem(L, ITEM) <- (
    ITEM > 0,
    Info(L, f"Processing item {ITEM}")
)
ProcessItem(L, ITEM) <- (
    ITEM =< 0,
    Warning(L, f"Skipping invalid item {ITEM}")
)
```

### Custom handler and formatter

```clausal

skip

-import_from(log, [
    GetLogger, Info, SetLevel,
    StreamHandler, FileHandler, SetFormatter, AddHandler
])

SetupLogging(L) <- (
    GetLogger("myapp", L),
    SetLevel(L, "debug"),
    FileHandler("/var/log/myapp.log", FH),
    SetFormatter(FH, "%(asctime)s [%(levelname)s] %(name)s: %(message)s"),
    AddHandler(L, FH),
    StreamHandler("stderr", SH),
    SetFormatter(SH, "%(levelname)s: %(message)s"),
    AddHandler(L, SH)
)
```

### Logger hierarchy

```clausal

skip

-import_from(log, [GetLogger, Info, SetLevel])

Setup <- (
    GetLogger("myapp", PARENT),
    SetLevel(PARENT, "info"),
    GetLogger("myapp.db", DBLOG),
    SetLevel(DBLOG, "debug"),
    Info(DBLOG, "DB logger inherits parent's handlers")
)
```

---
Implementation
  • Module: clausal/modules/log.py
  • Adapter class: _LoggingPredicate (same pattern as _RegexPredicate in clausal/modules/regex.py)
  • Backend: Python's logging module — all predicates delegate to logging.Logger methods
  • Tests: tests/test_logging_module.py (67 tests), tests/fixtures/logging_basic.clausal (30 fixture tests)

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 Clausal predicate succeeds regardless.
  3. IsEnabledFor/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; Clausal'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.