Logging ======= Xclif wires Python's standard :mod:`logging` to its built-in ``-v`` / ``--verbose`` flag. You write ordinary log calls; Xclif decides which records are shown and renders them with `Rich `_. .. note:: Xclif's logging is **not a separate logging system** — it is a thin convenience layer over Python's standard :mod:`logging`. ``log`` forwards to ``logging.getLogger``, ``configure_logging`` attaches a handler to the ordinary root logger, and every record flows through the normal ``logging`` tree. Anything you already know about the standard library — levels, handlers, filters, propagation, ``logging.getLogger(__name__)`` — applies unchanged. Xclif only adds the verbosity mapping, a Rich handler, and a couple of ergonomic helpers on top. Basic usage ----------- Import the bundled ``log`` and call it. During a command run, Xclif has already set the level from ``--verbose`` and attached a Rich handler: .. code-block:: python from xclif import command, log @command() def _(target: str) -> None: """Deploy to a target.""" log.info("Connecting to %s...", target) # shown at -v log.debug("Resolving %s...", target) # shown at -vv Run it: .. code-block:: bash myapp deploy prod # warnings and errors only myapp deploy prod -v # + info myapp deploy prod -vv # + debug, with file:line locations myapp deploy prod -vvv # + timestamps (verbose formatter) ``log`` provides the usual methods — ``debug``, ``info``, ``warning``, ``error``, ``critical``, ``exception``, and ``log`` — accepting the same ``%``-style arguments as the standard library. For a more modern, brace-style API see :ref:`modern-style logging ` below. It is not a logger of its own: on each call it resolves the **calling module's** ``__name__`` and forwards to ``logging.getLogger()``. So records carry the right source name and ``file:line`` no matter which module imported ``log``, and everything still flows through the standard :mod:`logging` tree. .. _modern-logging: Modern-style logging ~~~~~~~~~~~~~~~~~~~~~ The standard library's ``%``-style placeholders date back to 2003, before ``str.format`` (Python 2.6) and long before f-strings (3.6) existed — the ``logging`` API was simply built around the only formatting Python had at the time. An advantage of this approach over f-strings is that string formatting becomes lazy: the message is only interpolated if the record clears the active level, so a filtered-out ``log.debug("%s", value)`` costs nothing to format. .. code-block:: python log.info("connecting to %s on port %d", host, port) # stdlib, lazy But because ``str.format`` exists now, if you prefer modern brace formatting, ``log`` offers ``f``-prefixed variants — ``fdebug``, ``finfo``, ``fwarning``, ``ferror``, ``fcritical``, ``fexception``, and ``flog``. They take ``str.format`` (``{}`` / ``{name}``) placeholders and, as a bonus, **call any callable argument**. Both the formatting and the call are deferred until the record is actually emitted: .. code-block:: python log.finfo("connecting to {} on port {}", host, port) log.fdebug("state: {}", expensive_dump) # called only at -vv log.fdebug("state: {}", lambda: obj.to_json()) # lambda, same deal log.finfo("{user} from {host}", user=u, host=get_host) The callable support solves a problem ``%``-style cannot: ``%`` defers the *formatting* but not the *arguments*, so ``log.debug("%s", expensive_dump())`` runs ``expensive_dump()`` every time, even when filtered out. With the ``f``-variants you pass the callable itself (``expensive_dump``) — never its result (``expensive_dump()``) — and it is invoked only when needed. To log a callable's own repr, stringify it at the call site: ``log.fdebug("fn is {}", repr(fn))``. The same functions are available as :data:`~xclif.f` for code that prefers a free function over the ``log`` object — ``f.debug(...)`` is exactly ``log.fdebug(...)``: .. code-block:: python from xclif import f f.debug("state: {}", expensive_dump) .. tip:: f-strings work too — ``log.info(f"port {port}")`` — but they format **eagerly**, every time the line runs, even when the message is discarded. The f-variants keep brace syntax *and* stay lazy. When to use which +++++++++++++++++ - ``log.info`` / ``log.debug`` / ... — the default. Byte-for-byte compatible with the standard library, so existing code and ``logging`` linters keep working. Reach for these unless you have a reason not to. - ``log.finfo`` / ``log.fdebug`` / ... — when you want brace formatting, or when an argument is expensive to compute and should only run if the record is emitted. Skipping the ergonomic layer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You do not have to use ``log`` or ``f`` at all. If you prefer plain standard loggers — for an explicitly named logger, a per-component level, or simply to avoid Xclif-specific imports — just use :func:`logging.getLogger` directly: .. code-block:: python import logging logger = logging.getLogger(__name__) # the standard way logger.info("Connecting to %s...", target) logger.debug("Resolving %s...", target) This still gets Xclif's verbosity level and Rich rendering for free: your named logger propagates up to the **root** logger, which is where :func:`~xclif.configure_logging` sets the level and installs the handler. There is no extra wiring to do. ``get_logger`` is a thin convenience wrapper for readers who would rather import everything from ``xclif``; it returns the exact same standard logger: .. code-block:: python from xclif import get_logger logger = get_logger(__name__) # identical to logging.getLogger .. note:: This is also the path with the least overhead. The ``log`` object inspects the call stack on every call (via ``sys._getframe``) to recover the calling module's name and keep ``file:line`` accurate — the cost of being a single shared object. A logger you name yourself with ``getLogger(__name__)`` already knows its module, so it skips that work entirely. Verbosity-to-level mapping -------------------------- Each ``-v`` raises the verbosity count, which maps to a standard logging level and a progressively more detailed Rich formatter: .. list-table:: :header-rows: 1 :widths: 10 20 40 * - Flag - Level shown - Formatter detail * - *(none)* - ``WARNING`` - message only * - ``-v`` - ``INFO`` - message only * - ``-vv`` - ``DEBUG`` - + file/line locations * - ``-vvv`` - ``DEBUG`` (``NOTSET``) - + timestamps, traceback locals You can resolve the level yourself with :func:`~xclif.level_from_verbosity`, or read it off the dispatch context: .. code-block:: python from xclif import get_context, level_from_verbosity get_context().verbosity # raw count: 0–3 get_context().log_level # the implied logging level level_from_verbosity(2) # logging.DEBUG Integrating with Rich and the standard library ---------------------------------------------- Because Xclif only attaches a *handler* to the root logger, the two systems compose rather than compete: - **Any** standard-library logger flows through Rich — ``log`` is built on top of ``logging``, not beside it. Third-party libraries that log via ``logging`` are rendered through the same handler. - The handler is **lazy**: Rich is only imported the first time a record actually passes the level filter, keeping the startup hot path cheap. - Output goes to **stderr**, so it never pollutes a command's stdout. Cooperating with application-owned logging ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If your application has already configured its own handlers (for example, a file handler set up in ``__main__``), Xclif leaves them in place and only updates the logger level — it will not hijack your output. To force Xclif's Rich handler to replace existing handlers, call :func:`~xclif.configure_logging` yourself with ``force=True``: .. code-block:: python from xclif import configure_logging configure_logging(verbosity=2, colors="never", force=True) Note that even when existing handlers are respected, Xclif still **sets the root logger level** from ``--verbose`` on every dispatch. If you want to own the level too, opt out entirely (below) and set it yourself. Turning Xclif logging off entirely ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To stop Xclif from configuring logging at all — no level changes, no Rich handler — pass ``logging=False`` to :meth:`~xclif.Cli.from_routes` (or the ``Cli`` constructor). Logging is then 100% yours; the verbosity count is still available through :func:`~xclif.get_context`: .. code-block:: python import logging from xclif import Cli, get_context cli = Cli.from_routes(routes, logging=False) # In a command, wire up logging however you like: def run() -> None: level = logging.DEBUG if get_context().verbosity >= 2 else logging.INFO logging.getLogger().setLevel(level) ... Manual configuration --------------------- You rarely need to call :func:`~xclif.configure_logging` directly — Xclif invokes it during dispatch. It is exposed for tests, scripts, and apps that manage their own startup. Common overrides: .. code-block:: python from xclif import configure_logging # Pin an explicit level regardless of verbosity: configure_logging(level="ERROR") # Target a specific logger instead of the root: configure_logging(verbosity=1, logger="myapp") # Force timestamps off even at -vvv: configure_logging(verbosity=3, show_time=False) The ``colors`` argument mirrors the ``--colors`` flag (``"auto"``, ``"always"``, ``"never"``) so log output honors the same color preference as the rest of the CLI. See the :doc:`api` reference for the full signatures of :func:`~xclif.configure_logging`, :func:`~xclif.get_logger`, and :func:`~xclif.level_from_verbosity`.