Logging¶
A11 emits native C++ logs as ordinary Python logging.LogRecord values. The
native module forwards Abseil entries to the a11.native logger, where levels,
dictConfig, formatters, handlers, and pytest's caplog apply.
Importing A11 uses the process configuration¶
import a11 selects a level from the surrounding process in this order:
- absl-py, if the process configured it —
set_verbosity()was called,--verbositywas passed, orabsl.logging's handler is on the root logger. - The standard library — the effective level of the
a11logger, which inheritslogging.basicConfigandlogging.config.dictConfig. A11_LOG_LEVEL— a name (debug,info, ...) or an integer, read only when neither of the above says anything.
Without a configured level, A11 uses WARNING. Importing A11 prints nothing and
installs no handler.
Enabling logging explicitly¶
enable installs a handler only when the root logger has
none, and the one it installs is absl.logging's, so output keeps Abseil's
shape:
I0810 11:05:21.115550 8533073600 shell.py:64] opened shell 1
I0810 11:05:21.115635 8533073600 http2.cc:1691] HTTP/2 listener error
Under an application that has already configured logging, A11 installs nothing and its records — native ones included — flow into the handlers you set up.
set_level moves the a11 logger, absl.logging's
verbosity, and the native VLOG threshold together;
disable silences both sides.
Filtering and reconfiguration¶
Levels are resolved per record, so a setLevel or dictConfig that lands
after import takes effect with nothing to re-synchronise:
logging.getLogger("a11").setLevel(logging.DEBUG) # native logs included
logging.getLogger("a11.native").setLevel(logging.ERROR) # native logs only
A logging.Filter reaches native records the same way it reaches any other:
on a handler, or on the a11.native logger itself. Python does not consult an
ancestor logger's filters for a record that merely propagates through it, so
one attached to a11 will not see them.
Only VLOG is gated natively, because it is the one genuinely costly tier.
Call sync after external logging reconfiguration to pass a
sub-DEBUG level to the native runtime:
Levels below DEBUG¶
A11 follows absl-py's convention: a standard level under DEBUG selects an
Abseil VLOG tier, so logging.DEBUG - 1 enables VLOG(2).
What actions log¶
Action.log and Action.logf report action progress, while the logging bridge
reports runtime events. Action logs travel as chunks on a reserved port, so a
remote caller receives structured data instead of process-local stderr text.
await action.log("searching", channel="fetch")
await action.logf("read %d of %d pages", done, total)
await action.log({"hits": 12}, level="debug", internal=True)
The reserved port is created only when used and requires no schema declaration.
Only a running action may log; logging before run or from the calling side of
call has no active writer or reader.
What is consumed in this process becomes a record on the a11.action logger, so
setLevel, dictConfig and your existing handlers apply. The chunk's whole
description travels with it as record attributes -- a11_action, a11_channel,
a11_internal, a11_mimetype, a11_data -- so a handler can filter on the
channel or drop A11's internal lines without parsing the message back apart.
A11_ACTION_LOG=0 leaves them on the native log instead.
set_action_log_sink takes them somewhere else
entirely. A single sink prevents duplicate delivery. Calling
action.get_log_node() returns the chunks directly and suppresses the sink for
that action.
In Flow the same log is two statements and two pipeline stages:
log warning "no readable content"
logf "read %s pages" read after search
pages | log debug it -> kept
Escape hatch¶
A11_LOG_BRIDGE=0, or enable(bridge=False), leaves native entries on
Abseil's own stderr path instead of routing them through logging. Use it when
debugging the bridge, or when native threads must not touch the GIL.
LOG(FATAL) always goes straight to stderr with a backtrace, bridged or not:
it precedes process death, and there is nowhere else for it to go.
a11.logging
¶
Logging for A11, wired into the standard library's.
A11's runtime is C++, but its logs are ordinary logging.LogRecord values.
A sink inside the native module hands each Abseil entry to Python, which emits
it on the a11.native logger, so the whole of logging applies to native
output: levels, logging.config.dictConfig, pytest's caplog, a JSON
formatter, a file handler. There is no second logging system to configure.
Importing a11 reads the level from the surrounding process rather than
choosing one, in this order:
- absl-py, if the process configured it —
set_verbosity()was called,--verbositywas passed, orabsl.logging's handler is on the root logger. - The standard library — the effective level of the
a11logger, which inheritslogging.basicConfiganddictConfig. A11_LOG_LEVEL— a name (debug,info, ...) or an integer, read only when neither of the above says anything.
With none of those, the level is logging.WARNING, the same default a bare
interpreter gives you, and import a11 prints nothing.
To take control:
import a11
a11.enable_logging("debug") # or a11.logging.enable(logging.DEBUG)
a11.logging.get_logger(__name__).info("ready")
enable installs a handler only when the root logger has none, and the one it
installs is absl.logging's, so the text keeps Abseil's shape. Under an
application that has already configured logging, A11's records simply flow
into it.
A11_LOG_BRIDGE=0 (or enable(bridge=False)) leaves native entries on
Abseil's own stderr path instead of routing them through Python.
get_logger
¶
The a11 logger, or a child of it.
A dotted module name is placed under the A11 namespace, so
get_logger(__name__) inside a11.gateway.app yields
a11.gateway.app and inherits whatever is configured for a11.
parse_level
¶
Coerce level to a standard logging level.
Accepts an integer, a standard name ("DEBUG", "warning"), or a
decimal string. Values below logging.DEBUG select Abseil's VLOG
tiers, matching absl.logging's own convention.
set_action_log_sink
¶
Route what actions log somewhere other than A11's own logger.
callback(action_name, action_id, level, channel, file, lineno, internal,
mimetype, data, unix_seconds) is called for each log an action writes and
nothing else consumes -- an action whose log port a consumer claimed does
not reach a sink at all. None restores the default, which is a record on
the ACTION_LOGGER_NAME logger (or, with A11_ACTION_LOG=0, the native
log).
There is one slot rather than one sink per interested party, so a caller that takes it takes it from whoever had it. Nothing is reported twice as a result, which is the point.
set_level
¶
Set the level for A11's Python and native logging alike.
Applies to the a11 logger, to absl.logging's verbosity (which A11's
own modules log through), and to the native VLOG threshold. Returns
the standard level applied.
sync
¶
Re-read the effective Python level and push it to the runtime.
Records are filtered in Python, so this is only needed to pick up a
VLOG tier after reconfiguring logging behind A11's back — a
logging.config.dictConfig that puts the a11 logger below
logging.DEBUG.
enable
¶
Turn A11's logging on at level and return the level applied.
A handler is installed only when the root logger has none, and it is
absl.logging's, so output keeps Abseil's shape. Under an application
that already configured logging, this only sets levels.
Pass bridge=False to leave native entries on Abseil's own stderr path
rather than routing them through logging.