Usage
Setup
logstruct provides a formatter that formats all log records from standard library logging.Logger or its own StructuredLogger as JSON.
logging.basicConfig(level=logging.DEBUG)
logging.root.handlers[0].formatter = logstruct.StructuredFormatter()
logging.getLogger("stdlib-logger").info("hello world")
{"time": "...", "logger": "stdlib-logger", "level": "INFO", ..., "message": "hello world"}
Structured data
StructuredLogger is a replacement for the standard library
logging.Logger with an almost identical API. The key difference is that additional keyword
arguments provided to logging functions are automatically included in structured output.
logger = logstruct.getLogger(__name__)
logger.info("hello", request_id=1234, user="black_knight")
{..., "level": "INFO", ..., "message": "hello", "request_id": 1234, "user": "black_knight"}
This works for all logging levels.
logger.debug("good to know", request_id=1234)
logger.warning("take a look", request_id=1234)
logger.error("uh-oh", request_id=1234)
{..., "level": "DEBUG", ..., "message": "good to know", "request_id": 1234}
{..., "level": "WARNING", ..., "message": "take a look", "request_id": 1234}
{..., "level": "ERROR", ..., "message": "uh-oh", "request_id": 1234}
Alternatively, structured data can be passed as a dictionary in the extra parameter:
logger.info("hello", extra={"request_id": 1234, "user": "black_knight"})
{..., "message": "hello", "request_id": 1234, "user": "black_knight"}
Exception logging
Similar to logging.Logger, you can include exception information using the exc_info parameter:
logger.error("uh-oh", exc_info=ValueError("wrong value"), request_id=1234)
{..., "message": "uh-oh", "exc_text": "ValueError: wrong value", "request_id": 1234}
The currently handled exception is automatically captured if exc_info is true or if
logger.exception is called.
try:
x = 1 / 0
except ZeroDivisionError:
logger.debug("debug", exc_info=True, request_id=1234)
logger.exception("error", request_id=1234)
{..., "level": "DEBUG",... "message": "debug", "exc_text": "Traceback ...ZeroDivisionError: division by zero", "request_id": 1234}
{..., "level": "ERROR",... "message": "error", "exc_text": "Traceback ...ZeroDivisionError: division by zero", "request_id": 1234}
Logging config file (YAML)
version: 1
disable_existing_loggers: false
handlers:
stream:
class: logging.StreamHandler
formatter: structured
level: INFO
stream: ext://sys.stderr
formatters:
structured:
class: logstruct.StructuredFormatter
root:
level: INFO
handlers: [stream]
#!/usr/bin/env python3
"""Structured logging demo with dict config from YAML."""
import logging
import logging.config
from pathlib import Path
import yaml
import logstruct
config_file = Path(__file__).parent / "example_config.yaml"
with open(config_file) as f:
config = yaml.safe_load(f)
logging.config.dictConfig(config)
log = logstruct.getLogger(__name__)
log.info("An info message")
log.info("An info message with stack info", stack_info=True)
log.info("An info message with data (traditional)", extra={"log": "struct", "unrepresentable": logging})
log.info("An info message with data (kwargs)", log="struct", unrepresentable=logging)
try:
print(1 / 0)
except Exception:
log.exception("Division error")
{"time": "2025-03-02 01:07:23,832", "logger": "__main__", "level": "INFO", "func": "<module>", "line": 19, "message": "An info message"}
{"time": "2025-03-02 01:07:23,833", "logger": "__main__", "level": "INFO", "func": "<module>", "line": 20, "message": "An info message with stack info", "stack_info": "Stack (most recent call last):\n File \"/home/docs/checkouts/readthedocs.org/user_builds/logstruct/checkouts/0.1.1/docs/../demo_dict_config.py\", line 20, in <module>\n log.info(\"An info message with stack info\", stack_info=True)"}
{"time": "2025-03-02 01:07:23,833", "logger": "__main__", "level": "INFO", "func": "<module>", "line": 21, "message": "An info message with data (traditional)", "log": "struct", "unrepresentable": "<module 'logging' from '/home/docs/.asdf/installs/python/3.12.7/lib/python3.12/logging/__init__.py'>"}
{"time": "2025-03-02 01:07:23,833", "logger": "__main__", "level": "INFO", "func": "<module>", "line": 22, "message": "An info message with data (kwargs)", "log": "struct", "unrepresentable": "<module 'logging' from '/home/docs/.asdf/installs/python/3.12.7/lib/python3.12/logging/__init__.py'>"}
{"time": "2025-03-02 01:07:23,833", "logger": "__main__", "level": "ERROR", "func": "<module>", "line": 27, "message": "Division error", "exc_text": "Traceback (most recent call last):\n File \"/home/docs/checkouts/readthedocs.org/user_builds/logstruct/checkouts/0.1.1/docs/../demo_dict_config.py\", line 25, in <module>\n print(1 / 0)\n ~~^~~\nZeroDivisionError: division by zero"}
Logging config file (ini)
[loggers]
keys=root
[handlers]
keys=stream
[formatters]
keys=structured
[handler_stream]
class=logging.StreamHandler
formatter=structured
level=INFO
stream=ext://sys.stderr
[formatter_structured]
class=logstruct.StructuredFormatter
[logger_root]
level=INFO
handlers = stream
#!/usr/bin/env python3
"""Structured logging demo with file config."""
import logging
import logging.config
from pathlib import Path
import logstruct
config_file = Path(__file__).parent / "example_config.ini"
logging.config.fileConfig(config_file, disable_existing_loggers=False)
log = logstruct.getLogger(__name__)
log.info("An info message")
log.info("An info message with stack info", stack_info=True)
log.info("An info message with data (traditional)", extra={"log": "struct", "unrepresentable": logging})
log.info("An info message with data (kwargs)", log="struct", unrepresentable=logging)
try:
print(1 / 0)
except Exception:
log.exception("Division error")
{"time": "2025-03-02 01:07:23,897", "logger": "__main__", "level": "INFO", "func": "<module>", "line": 15, "message": "An info message"}
{"time": "2025-03-02 01:07:23,897", "logger": "__main__", "level": "INFO", "func": "<module>", "line": 16, "message": "An info message with stack info", "stack_info": "Stack (most recent call last):\n File \"/home/docs/checkouts/readthedocs.org/user_builds/logstruct/checkouts/0.1.1/docs/../demo_file_config.py\", line 16, in <module>\n log.info(\"An info message with stack info\", stack_info=True)"}
{"time": "2025-03-02 01:07:23,897", "logger": "__main__", "level": "INFO", "func": "<module>", "line": 17, "message": "An info message with data (traditional)", "log": "struct", "unrepresentable": "<module 'logging' from '/home/docs/.asdf/installs/python/3.12.7/lib/python3.12/logging/__init__.py'>"}
{"time": "2025-03-02 01:07:23,898", "logger": "__main__", "level": "INFO", "func": "<module>", "line": 18, "message": "An info message with data (kwargs)", "log": "struct", "unrepresentable": "<module 'logging' from '/home/docs/.asdf/installs/python/3.12.7/lib/python3.12/logging/__init__.py'>"}
{"time": "2025-03-02 01:07:23,898", "logger": "__main__", "level": "ERROR", "func": "<module>", "line": 23, "message": "Division error", "exc_text": "Traceback (most recent call last):\n File \"/home/docs/checkouts/readthedocs.org/user_builds/logstruct/checkouts/0.1.1/docs/../demo_file_config.py\", line 21, in <module>\n print(1 / 0)\n ~~^~~\nZeroDivisionError: division by zero"}
Context variables
Logstruct context variables are meant to contain data relevant to the current operation, like message ID, request path, user ID, job name, job ID, etc. Once set, they are shared by all subsequent logs until they are unset.
Context variables are automatically incorporated in produced structured logs but they can be overridden by
keyword args directly passed to log calls. Context variables are implemented with contextvars.Context
which makes them local to the current thread, asyncio task, or gevent task. Context variables are automatically inherited by child tasks but not by spawned threads.
You can use context_scope to add contextual information to all log
records within the scope.
with logstruct.context_scope(request_id="1234"):
logger.info("has context", user="abc")
logger.info("scope ended")
{..., "message": "has context", "request_id": "1234", "user": "abc"}
{..., "message": "scope ended"}
Contexts can be nested. Inner scopes shadow data from outer scopes.
with logstruct.context_scope(outer="outer", redefined="outer"):
with logstruct.context_scope(inner="inner", redefined="inner"):
logger.info("inner context")
{..., "message": "inner context", "inner": "inner", "redefined": "inner", "outer": "outer"}
You can modify the current context with add_context, remove_context, and clear_scope.
logstruct.add_context(request_id="1234")
logstruct.add_context(user="abc")
logger.info("has context")
logstruct.remove_context("user")
logger.info("only request_id")
logstruct.clear_scope()
logger.info("no context")
{..., "message": "has context", "request_id": "1234", "user": "abc"}
{..., "message": "only request_id", "request_id": "1234"}
{..., "message": "no context"}
Modifications only apply to the current scope. Once the scope ends, all modifications are lost.
with logstruct.context_scope(request_id="1234"):
logstruct.add_context(user="abc")
logger.info("modified")
logger.info("unmodified")
{..., "message": "modified", "request_id": "1234", "user": "abc"}
{..., "message": "unmodified"}
Highly customised config
#!/usr/bin/env python3
"""Structured logging demo with customisation."""
import json
import logging
from functools import partial
import logstruct
from logstruct import LogField, StructuredFormatter, StructuredFormatterConfig
logging.basicConfig(level=logging.INFO)
custom_json_formatter = StructuredFormatter(
datefmt="%Y-%m-%d %H:%M:%S",
structured_formatter_config=StructuredFormatterConfig(
format_message=False,
log_fields=[
LogField("asctime", "ts", bool),
LogField("name", "name"),
LogField("levelname", "level"),
LogField("module", "module"),
LogField("funcName", "func"),
LogField("lineno", "line"),
LogField("message", "event"),
LogField("exc_text", "exc_text", bool),
LogField("stack_info", "stack_info", bool),
LogField("args", "positional_args", bool),
],
# without `default=repr` it's very easy to cause serialisation errors
dumps_fn=partial(json.dumps, indent=4, default=repr),
),
)
logging.root.handlers[0].formatter = custom_json_formatter
log = logstruct.getLogger(__name__)
log.info("An info message")
log.info("An info message with stack info", stack_info=True)
log.info("An info message with data (traditional)", extra={"log": "struct", "unrepresentable": logging})
log.info("An info message with data (kwargs)", log="struct", unrepresentable=logging)
try:
print(1 / 0)
except Exception:
log.exception("Division error")
{
"ts": "2025-03-02 01:07:23",
"name": "__main__",
"level": "INFO",
"module": "demo_custom",
"func": "<module>",
"line": 37,
"event": "An info message"
}
{
"ts": "2025-03-02 01:07:23",
"name": "__main__",
"level": "INFO",
"module": "demo_custom",
"func": "<module>",
"line": 38,
"event": "An info message with stack info",
"stack_info": "Stack (most recent call last):\n File \"/home/docs/checkouts/readthedocs.org/user_builds/logstruct/checkouts/0.1.1/docs/../demo_custom.py\", line 38, in <module>\n log.info(\"An info message with stack info\", stack_info=True)"
}
{
"ts": "2025-03-02 01:07:23",
"name": "__main__",
"level": "INFO",
"module": "demo_custom",
"func": "<module>",
"line": 39,
"event": "An info message with data (traditional)",
"log": "struct",
"unrepresentable": "<module 'logging' from '/home/docs/.asdf/installs/python/3.12.7/lib/python3.12/logging/__init__.py'>"
}
{
"ts": "2025-03-02 01:07:23",
"name": "__main__",
"level": "INFO",
"module": "demo_custom",
"func": "<module>",
"line": 40,
"event": "An info message with data (kwargs)",
"log": "struct",
"unrepresentable": "<module 'logging' from '/home/docs/.asdf/installs/python/3.12.7/lib/python3.12/logging/__init__.py'>"
}
{
"ts": "2025-03-02 01:07:23",
"name": "__main__",
"level": "ERROR",
"module": "demo_custom",
"func": "<module>",
"line": 45,
"event": "Division error",
"exc_text": "Traceback (most recent call last):\n File \"/home/docs/checkouts/readthedocs.org/user_builds/logstruct/checkouts/0.1.1/docs/../demo_custom.py\", line 43, in <module>\n print(1 / 0)\n ~~^~~\nZeroDivisionError: division by zero"
}
Development mode logging
In this demo the DEBUG env var set to a non-empty value makes logs formatted in a developer-friendly way,
but unlike the default formatter, will include the extra dictionary, where our log call key-values go.
#!/usr/bin/env python3
"""Structured logging demo - dev-friendly console renderer."""
import logging
import os
import logstruct
from logstruct import (
StructuredFormatter,
StructuredFormatterConfig,
make_friendly_dump_fn,
)
logging.basicConfig(level=logging.DEBUG)
handler = logging.root.handlers[0]
if os.environ.get("DEBUG"):
handler.formatter = StructuredFormatter(
structured_formatter_config=StructuredFormatterConfig(
dumps_fn=make_friendly_dump_fn(
# Enable colours only if the handler pointed to a teletype.
colours=isinstance(handler, logging.StreamHandler) and handler.stream.isatty()
),
)
)
else:
handler.formatter = StructuredFormatter()
log = logstruct.getLogger(__name__)
log.debug("A debug message")
log.info("An info message")
log.warning("A warning message with stack info", stack_info=True)
log.error("An error message with data (traditional)", extra={"log": "struct", "unrepresentable": logging})
log.critical("A critical message with data (kwargs)", log="struct", unrepresentable=logging)
try:
print(1 / 0)
except Exception:
log.exception("Division error")
2025-03-02 01:07:23,996 DEBUG __main__:<module>:31 A debug message
2025-03-02 01:07:23,996 INFO __main__:<module>:32 An info message
2025-03-02 01:07:23,996 WARNING __main__:<module>:33 A warning message with stack info
Stack (most recent call last):
File "/home/docs/checkouts/readthedocs.org/user_builds/logstruct/checkouts/0.1.1/docs/../demo_dev_mode.py", line 33, in <module>
log.warning("A warning message with stack info", stack_info=True)
2025-03-02 01:07:23,996 ERROR __main__:<module>:34 An error message with data (traditional) {"log": "struct", "unrepresentable": "<module 'logging' from '/home/docs/.asdf/installs/python/3.12.7/lib/python3.12/logging/__init__.py'>"}
2025-03-02 01:07:23,996 CRITICAL __main__:<module>:35 A critical message with data (kwargs) {"log": "struct", "unrepresentable": "<module 'logging' from '/home/docs/.asdf/installs/python/3.12.7/lib/python3.12/logging/__init__.py'>"}
2025-03-02 01:07:23,996 ERROR __main__:<module>:40 Division error
Traceback (most recent call last):
File "/home/docs/checkouts/readthedocs.org/user_builds/logstruct/checkouts/0.1.1/docs/../demo_dev_mode.py", line 38, in <module>
print(1 / 0)
~~^~~
ZeroDivisionError: division by zero
{"time": "2025-03-02 01:07:24,044", "logger": "__main__", "level": "DEBUG", "func": "<module>", "line": 31, "message": "A debug message"}
{"time": "2025-03-02 01:07:24,044", "logger": "__main__", "level": "INFO", "func": "<module>", "line": 32, "message": "An info message"}
{"time": "2025-03-02 01:07:24,045", "logger": "__main__", "level": "WARNING", "func": "<module>", "line": 33, "message": "A warning message with stack info", "stack_info": "Stack (most recent call last):\n File \"/home/docs/checkouts/readthedocs.org/user_builds/logstruct/checkouts/0.1.1/docs/../demo_dev_mode.py\", line 33, in <module>\n log.warning(\"A warning message with stack info\", stack_info=True)"}
{"time": "2025-03-02 01:07:24,045", "logger": "__main__", "level": "ERROR", "func": "<module>", "line": 34, "message": "An error message with data (traditional)", "log": "struct", "unrepresentable": "<module 'logging' from '/home/docs/.asdf/installs/python/3.12.7/lib/python3.12/logging/__init__.py'>"}
{"time": "2025-03-02 01:07:24,045", "logger": "__main__", "level": "CRITICAL", "func": "<module>", "line": 35, "message": "A critical message with data (kwargs)", "log": "struct", "unrepresentable": "<module 'logging' from '/home/docs/.asdf/installs/python/3.12.7/lib/python3.12/logging/__init__.py'>"}
{"time": "2025-03-02 01:07:24,045", "logger": "__main__", "level": "ERROR", "func": "<module>", "line": 40, "message": "Division error", "exc_text": "Traceback (most recent call last):\n File \"/home/docs/checkouts/readthedocs.org/user_builds/logstruct/checkouts/0.1.1/docs/../demo_dev_mode.py\", line 38, in <module>\n print(1 / 0)\n ~~^~~\nZeroDivisionError: division by zero"}