This

import structlog
from structlog.contextvars import bound_contextvars
if __name__=="__main__":
    log = structlog.getLogger(__name__)
    with bound_contextvars(frame_id="ADA", command_id="123"):
        log.info("rabo")

logs 2023-11-04 21:17:15 [info ] rabo command_id=123 frame_id=ADA

This minimal example of what we actually use in production:

import os
from pathlib import Path
import logging
import logging.config

import structlog
from structlog.contextvars import bound_contextvars

LOG_DIR: Path = Path(".")
LOG_CONFIG_FILE: Path = Path("logging.conf")

STRUCTLOG_PROCESSORS = [
    structlog.stdlib.filter_by_level,
    structlog.stdlib.add_logger_name,
    structlog.stdlib.add_log_level,
    structlog.processors.CallsiteParameterAdder(
        [
            structlog.processors.CallsiteParameter.THREAD_NAME,
            structlog.processors.CallsiteParameter.PROCESS_NAME,
            structlog.processors.CallsiteParameter.PATHNAME,
        ]
    ),
    structlog.stdlib.PositionalArgumentsFormatter(),
    structlog.processors.TimeStamper(fmt="ISO", key="@timestamp"),
    structlog.processors.UnicodeDecoder(),
    structlog.processors.ExceptionRenderer(),
    structlog.processors.StackInfoRenderer(),
    structlog.processors.EventRenamer("message"),
    structlog.processors.JSONRenderer(),
]

DEFAULT_EVENT_FIELDS = {
    "appVersion": "unset",
    "supervisor_process_name": "not_set",
    "hostname": None,
    ## This causes log line    "frame_id": null, "command_id": null, "message": "rabo"
    # "frame_id": None,
    # "command_id": None,
}


def _configure_logging(processors) -> None:
    LOG_DIR.mkdir(parents=True, exist_ok=True)
    os.environ["LOG_DIR"] = str(LOG_DIR)
    logging.config.fileConfig(LOG_CONFIG_FILE, disable_existing_loggers=False)

    structlog.configure(
        processors=processors,
        wrapper_class=structlog.stdlib.BoundLogger,
        logger_factory=structlog.stdlib.LoggerFactory(),
        cache_logger_on_first_use=True,
    )


def add_custom_fields(logger, method_name, event_dict):  # pylint: disable=unused-argument
    return {**event_dict, **DEFAULT_EVENT_FIELDS}


_configure_logging([add_custom_fields] + STRUCTLOG_PROCESSORS)


if __name__=="__main__":
    log = structlog.getLogger(__name__)
    with bound_contextvars(frame_id="ADA", command_id="123"):
        log.info("rabo")

with logging.conf

[loggers]
keys=root

[handlers]
keys=consoleHandler,consoleHandlerErr

[formatters]
keys=Formatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=Formatter
args=(sys.stdout,)

[handler_consoleHandlerErr]
class=StreamHandler
level=DEBUG
formatter=Formatter
args=(sys.stderr,)

[formatter_Formatter]
format=%(message)s

logs {"appVersion": "unset", "supervisor_process_name": "not_set", "hostname": null, "logger": "__main__", "level": "info", "thread_name": "MainThread", "process_name": "n/a", "pathname": "/Users/dario.figueira/Library/Application Support/JetBrains/PyCharm2023.1/scratches/bound_contextvars.py", "@timestamp": "2023-11-04T21:19:17.083187Z", "message": "rabo"}

What's missing so that the bound context vars show up in the log lines? Adding frame_id and command_id to DEFAULT_EVENT_FIELDS was not enough

EDIT: Adding structlog.contextvars.merge_contextvars, after STRUCTLOG_PROCESSORS = [ fixes it

1

There are 1 best solutions below

0
On BEST ANSWER

Your configuration is missing the structlog.contextvars.merge_contextvars processor that is part of the default configuration.