How it works
Components
The following logging and Logstruct components work together to provide a feature-complete structured logging solution.
logstruct.StructuredLogger- Allows to perform log calls with arbitrary keyword args attached.logstruct.StructuredFormatterConfig- Defines all logstruct-specificStructuredFormatterconfiguration.logstruct.LogField- Maps LogRecord attributes to key-value pairs
logging.LogRecord- Contains all data produced by a log call, standard library or logstruct.logging.Logger- Basic building block of standard library logging topology.logging.Handler- Called by loggers provided with handlers (typically the root logger only).logstruct.StructuredFormatter- Formats log records as data.
High-level flow
A standard logging configuration with logstruct enabled works as follows:
logstruct.StructuredLoggeris called by user codelogging.Loggeris called bylogstruct.StructuredLoggerLog propagation according to the configured stdlib logging topology takes place - check Logging Flow
Final
logging.Loggeris reached (typicallylogging.root)logging.Handler(typicallylogging.StreamHandler) is called by the final logger.logstruct.StructuredLoggeris called by the handler. It renders a structured log (JSON by default) and returns to the handler.
The handler outputs the structured log.
This configuration is identical to a typical stdlib logging configuration, except it adds
logstruct.StructuredLogger at the beginning and it attaches logstruct.StructuredFormatter to
the target log handler.
Detail
Calling the logger and producing a LogRecord (points 1-2)
A Logstruct logger is called with arbitrary keyword arguments. It packages those keyword args in a
dict and passes it as the extra argument to its underlying standard library logger.
Therefore this code:
log = logstruct.getLogger("logger_name")
log.info("Message", key="value", count=22)
is equivalent to:
log = logging.getLogger("logger_name")
log.info("Message", extra={"key": "value", "count": 22})
In logging not all strings are valid extra dict keys - namely, if LogRecord attributes
are passed in extra, KeyError: "Attempt to overwrite '<sth>' in LogRecord" is thrown.
In Logstruct this is fixed by prefixing conflicting keys with "logstruct_key_".
LogRecord propagation (points 3-4)
LogRecord building and propagation happens completely in the standard library logging module.
Once the standard library logger is called with the extra dictionary carrying the user-supplied
key-vals, this dictionary is merged into log_record.__dict__, creating arbitrary LogRecord
attributes.
The created LogRecord gets propagated as in the Logging Flow until it reaches a logger that
handles it by passing it onto a handler configured with a StructuredFormatter instance. Typically
this is logging.root.
Handling, formatting, and output (point 5-6)
Once the LogRecord is passed onto a logging handler with StructuredFormatter attached, the
StructuredFormatter processes the LogRecord.
It formats the message, timestamp and optional exception info contained in the
LogRecord. This mutates theLogRecord, which is equivalent to the behaviour oflogging.Formatter.It picks up standard
LogRecordattributes and places them in a temporary dictionary. Attributes can be included, excluded or conditionally included in the produced dictionary. This is configured byLogFieldentries in theStructuredFormatterConfigstruct passed to theStructuredFormatter.The temp dictionary is updated with the current Logstruct context dict.
The temp dictionary is updated with non-default
LogRecordattributes.Those attributes come from the arbitrary keyword args or the explicit
extraarg originally passed in theStructuredLoggercall. Either way, the kwargs and extras get flattened and passed tologging.Logger. After theloggingmachinery creates alogging.LogRecord, it merges them into the log record’s attributes.Another annoyance is that not all keys can be merged into
logging.LogRecordattributes, which is we compensated for in step 1 by prefixing conflicting keys. Now we have undo it.This step solves the confusion introduced by the stdlib
logginglibrary’s way of handling theextradictionary.The temp dictionary is serialised as JSON or some other format, depending on the
StructuredFormatterConfig.dumps_fnattribute.
This completes the process of turning logging into a complete structured logging library.