Skip to content

Logging

Clak can configure stderr logging, -v verbosity tiers, and a per-parser self.logger via LoggingOptMixin.

Runnable example: examples/script_logging.py.

Two approaches

Pick one ownership model; do not mix them for the same process.

Clak manages logging — inherit LoggingOptMixin. Clak configures stderr handlers, formatters, -v tiers from Meta.log_levels, and binds self.logger. Use this for new CLIs or when you want Clak’s logging UX.

App manages logging — use Parser only (omit LoggingOptMixin). Keep your own logging setup (basicConfig, dictConfig, handlers, libraries). Clak does not call dictConfig. Wire -v (or equivalent) yourself if you need verbosity flags.

Custom Clak levels (SPAM, VERBOSE, SUCCESS, NOTICE) register only when something imports the logging component / mixin path; they are additive on the stdlib logging module and usually harmless next to an app-owned setup.

Quick start

from clak import LoggingOptMixin, Parser

class App(LoggingOptMixin, Parser):
    class Meta:
        log_prefix = __name__          # names self.logger (recommended)
        log_default_level = "WARNING"  # root logger level
        log_levels = [
            ["WARNING|myapp"],         # default (no -v)
            ["INFO|myapp"],            # -v
            ["DEBUG|myapp"],           # -vv
            ["DEBUG|"],                # -vvv (empty name = root)
        ]
        log_silent = ["urllib3"]       # WARNING until max -v

    def cli_run(self, **_):
        self.logger.info("ready")
        self.logger.success("ok")      # custom Clak level
script_logging.py
#!/usr/bin/env python3
"""Demo: structured CLI logging with LoggingOptMixin.

Try:
  ./script_logging.py
  ./script_logging.py -v
  ./script_logging.py -vv
  ./script_logging.py -vvv
  ./script_logging.py -vv --log-format extended
  ./script_logging.py greet Ada
"""

from __future__ import annotations

import logging

from clak import Argument, Command, LoggingOptMixin, Parser

logger = logging.getLogger(__name__)


class GreetCmd(Parser):
    """Greet someone and emit logs at several levels."""

    name = Argument("NAME", help="Name to greet", default="World", nargs="?")

    def cli_run(self, name="World", **_):
        self.logger.debug("Preparing greeting for %s", name)
        self.logger.info("Hello, %s", name)
        self.logger.success("Greeting delivered")
        self.logger.notice("Tip: pass -v / -vv for more detail")
        print(f"Hello, {name}!")


class AppMain(LoggingOptMixin, Parser):
    """Logging demo.

    ``Meta.log_levels`` lists cumulative ``-v`` tiers as ``LEVEL|logger``.
    An empty logger name configures the root logger.
    """

    class Meta:
        log_prefix = __name__
        log_default_level = "WARNING"
        log_levels = [
            ["WARNING|" + __name__],  # default
            ["INFO|" + __name__],  # -v
            ["DEBUG|" + __name__],  # -vv
            ["DEBUG|"],  # -vvv (root — includes clak internals)
        ]
        # Kept at WARNING until maximum verbosity (-vvv here)
        log_silent = ["urllib3", "asyncio"]

    greet = Command(GreetCmd)

    def cli_run(self, **_):
        logger.warning("App module logger (logging.getLogger(__name__))")
        self.logger.info("Parser logger (self.logger) — visible from -v")
        self.logger.debug("Debug line — visible from -vv")
        print("Run a subcommand, e.g. greet — or pass -h")


if __name__ == "__main__":
    AppMain()
$ python script_logging.py
[ WARNING] App module logger (logging.getLogger(__name__))
Run a subcommand, e.g. greet — or pass -h

$ python script_logging.py -v greet Ada
[    INFO] Hello, Ada
[ SUCCESS] Greeting delivered
[  NOTICE] Tip: pass -v / -vv for more detail
Hello, Ada!

$ python script_logging.py -vv greet Ada
[   DEBUG] Preparing greeting for Ada
[    INFO] Hello, Ada
[ SUCCESS] Greeting delivered
[  NOTICE] Tip: pass -v / -vv for more detail
Hello, Ada!

$ python script_logging.py -vv --log-format extended greet Ada
[   DEBUG] __main__.GreetCmd: Preparing greeting for Ada
[    INFO] __main__.GreetCmd: Hello, Ada
[ SUCCESS] __main__.GreetCmd: Greeting delivered
[  NOTICE] __main__.GreetCmd: Tip: pass -v / -vv for more detail
Hello, Ada!

CLI flags

Flag Values Default Effect
-v / --verbose count (-v, -vv, …) 0 Select cumulative Meta.log_levels tier
--log-format default, extended, audit, debug default Formatter style
--trace / --no-trace bool False Show traceback before the exception handler chain
--log-colors / --no-log-colors bool auto Colored output when on (needs coloredlogs; default: on for TTY)

Install colors:

pip install 'mrjk.clak[colors]'
# or: pip install coloredlogs

Default for --log-colors when the flag is omitted:

  1. Env named by Meta.log_colors_env if set (default: CLAK_LOG_COLORS)
  2. Else on when CLAK_COLORS is true and stderr is a TTY

Explicit --log-colors / --no-log-colors always wins. Without coloredlogs, the flag is still present and colors fall back to plain formatting.

Apps that brand their own env vars (without patching Clak globals) set:

class Meta:
    log_colors_env = "PAASIFY__LOG_COLORS"

That updates --help and the resolve path together.

Meta settings

Setting Purpose
log_prefix Base name for self.logger (typically __name__). If omitted, the parser module name is used.
log_suffix How the right-hand side of the logger name is built (see below).
log_default_level Root logger level (WARNING by default). String or int.
log_levels List of cumulative -v tiers. Each tier is a list of LEVEL\|logger entries.
log_silent Logger names forced to WARNING until maximum verbosity.
log_colors_env Env var name for --log-colors default and help text (CLAK_LOG_COLORS by default).

log_levels syntax

Each entry is LEVEL|logger_name:

  • LEVEL — any stdlib or Clak level name (INFO, DEBUG, SUCCESS, …)
  • logger_name — dotted logger name; empty (INFO|) means the root logger

Tiers are cumulative: -vv applies tier 0, then 1, then 2 (later entries override earlier ones for the same logger).

Legacy form (still supported): plain logger names only, e.g. [["clak"], [""]]. Clak expands each group into INFO then DEBUG tiers.

If log_levels is omitted, Clak uses:

[
    ["INFO|clak"],
    ["DEBUG|clak"],
    ["INFO|"],
    ["DEBUG|"],
]

log_suffix modes

Value Logger name
omitted / None {log_prefix}.{ClassName} (same as "==FLAT==")
"==FLAT==" {log_prefix}.{ClassName}
"==NESTED==" {log_prefix} + dotted command path
argparse.SUPPRESS {log_prefix} only (no suffix)
any other string {log_prefix} + that suffix (a leading . is added if missing)

Without log_prefix, self.logger uses the parser class module name (suffix rules still apply only when a prefix is set).

Custom levels

Clak registers these levels on import of the logging component (and when CLAK_DEBUG=1):

Level Value Method
SPAM 5 logger.spam(...)
VERBOSE 15 logger.verbose(...)
SUCCESS INFO+3 logger.success(...)
NOTICE INFO+5 logger.notice(...)

They work on logging, Logger, and LoggerAdapter. Styles for colored output live next to the level definitions and are merged into LOG_STYLES.

Advanced: register your own with clak.log_levels.add_logging_level / register_clak_log_levels.

Environment variables

Variable Effect
CLAK_DEBUG=1 Enable library debug logging early; also forces --trace behavior in dispatch()
CLAK_LOG_COLORS=0 or 1 Default for --log-colors when the flag is omitted (overrides TTY auto); rename via Meta.log_colors_env
CLAK_COLORS=0 Hard kill-switch: skip coloredlogs import and other Clak color integration

Common patterns

Keep your existing logging configuration. Omit LoggingOptMixin so Clak does not install handlers or call dictConfig.

import logging
from clak import Parser  # no LoggingOptMixin

logging.basicConfig(level=logging.INFO)  # or your existing setup
logger = logging.getLogger(__name__)

class App(Parser):
    def cli_run(self, **_):
        logger.info("app logging stays as configured")
class App(LoggingOptMixin, Parser):
    class Meta:
        log_prefix = "myapp"
        log_levels = [
            ["INFO|myapp"],
            ["DEBUG|myapp"],
            ["DEBUG|myapp", "INFO|"],
        ]
        log_silent = ["urllib3", "requests"]
import logging
from clak import LoggingOptMixin, Parser

logger = logging.getLogger(__name__)

class App(LoggingOptMixin, Parser):
    class Meta:
        log_prefix = __name__

    def cli_run(self, **_):
        logger.info("usual module logger")
        self.logger.info("parser-bound logger (%s)", self.logger.name)
class Child(Parser):
    def cli_run(self, **_):
        self.logger.info("runs under the parent LoggingOptMixin config")

class App(LoggingOptMixin, Parser):
    class Meta:
        log_prefix = "myapp"
        log_suffix = "==NESTED=="
        log_levels = [["INFO|myapp"], ["DEBUG|myapp"]]

    child = Command(Child)

See also