contextuallogging
Pure-Python semantic logging library with context-local values.
Installation
This package is available on PyPI and can be installed with pip:
pip install contextuallogging
1# Copyright 2023 Charles Andrews 2# fmt: off 3"""[](https://pypi.org/project/contextuallogging/) 4[](https://github.com/cfandrews/PythonContextualLogging/actions) 5[](https://github.com/cfandrews/PythonContextualLogging/actions) 6[](https://mypy-lang.org/) 7[](https://github.com/astral-sh/ruff) 8 9Pure-Python semantic logging library with context-local values. 10 11[PyPI](https://pypi.org/project/contextuallogging/) 12[Source](https://github.com/cfandrews/PythonContextualLogging/) 13[Documentation](https://cfandrews.github.io/PythonContextualLogging/contextuallogging.html) 14 15## Installation 16This package is available on PyPI and can be installed with pip: 17```shell 18pip install contextuallogging 19``` 20""" # noqa: W291, D205, D415 21# fmt: on 22 23from __future__ import annotations 24 25from typing import Final 26 27from contextuallogging._context import context 28from contextuallogging._context_management import ( 29 get_context, 30 reset_context, 31 set_context_key, 32) 33from contextuallogging._contextual_formatter import ContextualFormatter 34 35__all__: Final[list[str]] = [ 36 "context", 37 "ContextualFormatter", 38 "get_context", 39 "set_context_key", 40 "reset_context", 41] 42__docformat__: Final[str] = "google"
17def context( 18 function: Callable[..., Any] | None = None, 19 /, 20 *, 21 keyword: str, 22 key: str | None = None, 23) -> Callable[..., Any]: 24 """Decorator which adds logging context during a function call. 25 26 The logging context is added prior to calling the wrapped function and is reset 27 after the function returns, creating a stack-like context in which the context added 28 here will propagate to all logging calls made in all sub-functions. 29 30 Existing keys in the logging context will be overwritten by this decorator if they 31 are already present, but the context will be reset to its original state once the 32 wrapped function returns. 33 34 This decorator supports decorating async functions, as well. As this package uses 35 ContextVars behind-the-scenes, the logging context can be propagated to async 36 function calls in some cases and follows the same rules as ContextVars as to when 37 that happens. 38 39 The created wrapper function raises a RuntimeError if the provided keyword name is 40 not actually present in the passed-in kwargs. 41 42 Example usage: 43 44 >>> import sys 45 >>> import logging 46 >>> from contextuallogging import context, ContextualFormatter 47 >>> 48 >>> logger = logging.getLogger() 49 >>> handler = logging.StreamHandler(sys.stdout) 50 >>> handler.setFormatter(ContextualFormatter()) 51 >>> logger.addHandler(handler) 52 >>> logger.setLevel(logging.INFO) 53 >>> 54 >>> @context(keyword="username", key="user") 55 ... def inner_function(username: str) -> None: 56 ... logger.info("inner_function") 57 ... 58 >>> @context(keyword="request_id") 59 ... def outer_function(request_id: str) -> None: 60 ... logger.info("outer_function") 61 ... inner_function(username="cfandrews") 62 ... logger.info("outer_function again") 63 ... 64 >>> outer_function(request_id="7fb9b341") 65 {"level": "INFO", "logger": "root", "message": "outer_function", "request_id": "7fb9b341", "timestamp": "2023-11-25T20:56:41.796564Z"} 66 {"level": "INFO", "logger": "root", "message": "inner_function", "request_id": "7fb9b341", "timestamp": "2023-11-25T20:56:41.797024Z", "user": "cfandrews"} 67 {"level": "INFO", "logger": "root", "message": "outer_function again", "request_id": "7fb9b341", "timestamp": "2023-11-25T20:56:41.797075Z"} 68 69 Args: 70 function (Callable[..., Any]): The function wrapped by this decorator. 71 keyword (str): The name of the keyword argument to add to the logging context. 72 key (str | None): The key to use in the logging context. 73 """ # noqa: E501 74 if function is None: 75 return partial(context, keyword=keyword, key=key) 76 77 if inspect.iscoroutinefunction(function): 78 79 @wraps(function) 80 async def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 81 if keyword not in kwargs: 82 message: Final[str] = f"Keyword argument not provided: {keyword}" 83 raise RuntimeError(message) 84 token: Final[Token[dict[str, object]]] = set_context_key( 85 key=keyword if key is None else key, 86 value=kwargs[keyword], 87 ) 88 result: Final[Any] = await function(*args, **kwargs) 89 reset_context(token=token) 90 return result 91 92 else: 93 94 @wraps(function) 95 def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 96 if keyword not in kwargs: 97 message: Final[str] = f"Keyword argument not provided: {keyword}" 98 raise RuntimeError(message) 99 token: Final[Token[dict[str, object]]] = set_context_key( 100 key=keyword if key is None else key, 101 value=kwargs[keyword], 102 ) 103 result: Final[Any] = function(*args, **kwargs) 104 reset_context(token=token) 105 return result 106 107 return wrapper
Decorator which adds logging context during a function call.
The logging context is added prior to calling the wrapped function and is reset after the function returns, creating a stack-like context in which the context added here will propagate to all logging calls made in all sub-functions.
Existing keys in the logging context will be overwritten by this decorator if they are already present, but the context will be reset to its original state once the wrapped function returns.
This decorator supports decorating async functions, as well. As this package uses ContextVars behind-the-scenes, the logging context can be propagated to async function calls in some cases and follows the same rules as ContextVars as to when that happens.
The created wrapper function raises a RuntimeError if the provided keyword name is not actually present in the passed-in kwargs.
Example usage:
>>> import sys
>>> import logging
>>> from contextuallogging import context, ContextualFormatter
>>>
>>> logger = logging.getLogger()
>>> handler = logging.StreamHandler(sys.stdout)
>>> handler.setFormatter(ContextualFormatter())
>>> logger.addHandler(handler)
>>> logger.setLevel(logging.INFO)
>>>
>>> @context(keyword="username", key="user")
... def inner_function(username: str) -> None:
... logger.info("inner_function")
...
>>> @context(keyword="request_id")
... def outer_function(request_id: str) -> None:
... logger.info("outer_function")
... inner_function(username="cfandrews")
... logger.info("outer_function again")
...
>>> outer_function(request_id="7fb9b341")
{"level": "INFO", "logger": "root", "message": "outer_function", "request_id": "7fb9b341", "timestamp": "2023-11-25T20:56:41.796564Z"}
{"level": "INFO", "logger": "root", "message": "inner_function", "request_id": "7fb9b341", "timestamp": "2023-11-25T20:56:41.797024Z", "user": "cfandrews"}
{"level": "INFO", "logger": "root", "message": "outer_function again", "request_id": "7fb9b341", "timestamp": "2023-11-25T20:56:41.797075Z"}
Arguments:
- function (Callable[..., Any]): The function wrapped by this decorator.
- keyword (str): The name of the keyword argument to add to the logging context.
- key (str | None): The key to use in the logging context.
15class ContextualFormatter(Formatter): 16 """Logging Formatter which can impart contextual information onto a LogRecord. 17 18 This class can be used in the same way as any regular logging formatter but will add 19 any fields set by the contextual logging decorators. 20 21 This class is opinionated on its format. Specifically, it outputs log messages as 22 JSON key-value mappings and all logged objects must be encode-able by the provided 23 JSON encoder. 24 25 By default, the formatted logs will include the fields: 26 * `timestamp` 27 * `level` 28 * `logger` 29 * `message` 30 * `exception` 31 * If `exc_info` present on the LogRecord 32 * `stack` 33 * If `stack_info` present on the LogRecord 34 35 Example usage: 36 37 >>> import logging 38 >>> import sys 39 >>> from contextuallogging import ContextualFormatter 40 >>> 41 >>> logger = logging.getLogger() 42 >>> handler = logging.StreamHandler(sys.stdout) 43 >>> handler.setFormatter(ContextualFormatter()) 44 >>> logger.addHandler(handler) 45 >>> logger.setLevel(logging.INFO) 46 >>> 47 >>> logger.info("Lorem ipsum dolor sit amet") 48 {"level": "INFO", "logger": "root", "message": "Lorem ipsum dolor sit amet", "timestamp": "2023-11-25T22:25:42.803579Z"} 49 """ # noqa: E501 50 51 def __init__( 52 self, 53 encoder: JSONEncoder | None = None, 54 ) -> None: 55 """Constructor. 56 57 The default encoder is instantiated as: 58 59 ```python 60 JSONEncoder( 61 skipkeys=True, 62 ensure_ascii=False, 63 sort_keys=True, 64 ) 65 ``` 66 67 Args: 68 encoder (JSONEncoder | None): An optional JSONEncoder to use for formatting 69 log messages. 70 """ 71 self.__encoder: Final[JSONEncoder] = ( 72 JSONEncoder(skipkeys=True, ensure_ascii=False, sort_keys=True) 73 if encoder is None 74 else encoder 75 ) 76 super().__init__() 77 78 def format(self, record: LogRecord) -> str: # noqa: A003 79 """Format the given record as a string. 80 81 Args: 82 record (LogRecord): The LogRecord to format. 83 84 Returns: 85 str: The formatted LogRecord. 86 """ 87 data: Final[dict[str, object]] = {} 88 timestamp: Final[datetime] = datetime.fromtimestamp( 89 record.created, 90 tz=timezone.utc, 91 ) 92 data["timestamp"] = ( 93 f"{timestamp.year:04}-{timestamp.month:02}-{timestamp.day:02}T" 94 f"{timestamp.hour:02}:{timestamp.minute:02}:{timestamp.second:02}." 95 f"{timestamp.microsecond:06}Z" 96 ) 97 data["level"] = record.levelname 98 data["logger"] = record.name 99 data["message"] = record.getMessage() 100 if record.exc_info is not None: 101 data["exception"] = record.exc_text 102 if record.stack_info is not None: 103 data["stack"] = self.formatStack(stack_info=record.stack_info) 104 return self.__encoder.encode({**data, **get_context()})
Logging Formatter which can impart contextual information onto a LogRecord.
This class can be used in the same way as any regular logging formatter but will add any fields set by the contextual logging decorators.
This class is opinionated on its format. Specifically, it outputs log messages as JSON key-value mappings and all logged objects must be encode-able by the provided JSON encoder.
By default, the formatted logs will include the fields:
timestamplevelloggermessageexception- If
exc_infopresent on the LogRecord
- If
stack- If
stack_infopresent on the LogRecord
- If
Example usage:
>>> import logging
>>> import sys
>>> from contextuallogging import ContextualFormatter
>>>
>>> logger = logging.getLogger()
>>> handler = logging.StreamHandler(sys.stdout)
>>> handler.setFormatter(ContextualFormatter())
>>> logger.addHandler(handler)
>>> logger.setLevel(logging.INFO)
>>>
>>> logger.info("Lorem ipsum dolor sit amet")
{"level": "INFO", "logger": "root", "message": "Lorem ipsum dolor sit amet", "timestamp": "2023-11-25T22:25:42.803579Z"}
51 def __init__( 52 self, 53 encoder: JSONEncoder | None = None, 54 ) -> None: 55 """Constructor. 56 57 The default encoder is instantiated as: 58 59 ```python 60 JSONEncoder( 61 skipkeys=True, 62 ensure_ascii=False, 63 sort_keys=True, 64 ) 65 ``` 66 67 Args: 68 encoder (JSONEncoder | None): An optional JSONEncoder to use for formatting 69 log messages. 70 """ 71 self.__encoder: Final[JSONEncoder] = ( 72 JSONEncoder(skipkeys=True, ensure_ascii=False, sort_keys=True) 73 if encoder is None 74 else encoder 75 ) 76 super().__init__()
Constructor.
The default encoder is instantiated as:
JSONEncoder(
skipkeys=True,
ensure_ascii=False,
sort_keys=True,
)
Arguments:
- encoder (JSONEncoder | None): An optional JSONEncoder to use for formatting log messages.
78 def format(self, record: LogRecord) -> str: # noqa: A003 79 """Format the given record as a string. 80 81 Args: 82 record (LogRecord): The LogRecord to format. 83 84 Returns: 85 str: The formatted LogRecord. 86 """ 87 data: Final[dict[str, object]] = {} 88 timestamp: Final[datetime] = datetime.fromtimestamp( 89 record.created, 90 tz=timezone.utc, 91 ) 92 data["timestamp"] = ( 93 f"{timestamp.year:04}-{timestamp.month:02}-{timestamp.day:02}T" 94 f"{timestamp.hour:02}:{timestamp.minute:02}:{timestamp.second:02}." 95 f"{timestamp.microsecond:06}Z" 96 ) 97 data["level"] = record.levelname 98 data["logger"] = record.name 99 data["message"] = record.getMessage() 100 if record.exc_info is not None: 101 data["exception"] = record.exc_text 102 if record.stack_info is not None: 103 data["stack"] = self.formatStack(stack_info=record.stack_info) 104 return self.__encoder.encode({**data, **get_context()})
Format the given record as a string.
Arguments:
- record (LogRecord): The LogRecord to format.
Returns:
str: The formatted LogRecord.
Inherited Members
- logging.Formatter
- converter
- datefmt
- default_time_format
- default_msec_format
- formatTime
- formatException
- usesTime
- formatMessage
- formatStack
16def get_context() -> dict[str, object]: 17 """Gets the current logging context. 18 19 This is an advanced utility function which is not necessary for most use-cases. 20 21 Example usage: 22 23 >>> from contextuallogging import get_context, set_context_key 24 >>> get_context() 25 {} 26 >>> _ = set_context_key(key="key", value="value") 27 >>> get_context() 28 {'key': 'value'} 29 30 Returns: 31 dict[str, object]: The current logging context. 32 """ 33 return _context.get()
Gets the current logging context.
This is an advanced utility function which is not necessary for most use-cases.
Example usage:
>>> from contextuallogging import get_context, set_context_key
>>> get_context()
{}
>>> _ = set_context_key(key="key", value="value")
>>> get_context()
{'key': 'value'}
Returns:
dict[str, object]: The current logging context.
36def set_context_key(key: str, value: object) -> Token[dict[str, object]]: 37 """Set the given key-value pair in the logging context. 38 39 This is an advanced utility function which is not necessary for most use-cases. 40 41 The returned `contextvars.Token` object can be passed to `reset_context` to reset 42 the logging context to its state as it was prior to this operation. 43 44 Example usage: 45 46 >>> from contextuallogging import get_context, set_context_key 47 >>> get_context() 48 {} 49 >>> _ = set_context_key(key="key", value="value") 50 >>> get_context() 51 {'key': 'value'} 52 53 Args: 54 key (str): The key of the logging field. 55 value (object): The value of the logging field. 56 57 Returns: 58 Token[dict[str, object]]: The Token required to reset the logging context to the 59 previous state. 60 """ 61 return _context.set({**get_context(), key: value})
Set the given key-value pair in the logging context.
This is an advanced utility function which is not necessary for most use-cases.
The returned contextvars.Token object can be passed to reset_context to reset
the logging context to its state as it was prior to this operation.
Example usage:
>>> from contextuallogging import get_context, set_context_key
>>> get_context()
{}
>>> _ = set_context_key(key="key", value="value")
>>> get_context()
{'key': 'value'}
Arguments:
- key (str): The key of the logging field.
- value (object): The value of the logging field.
Returns:
Token[dict[str, object]]: The Token required to reset the logging context to the previous state.
64def reset_context(token: Token[dict[str, object]]) -> None: 65 """Reset the logging context with the given token. 66 67 This is an advanced utility function which is not necessary for most use-cases. 68 69 The given token should be obtained from a `set_context_key` call and should never be 70 constructed from scratch manually. 71 72 Example usage: 73 74 >>> from contextuallogging import get_context, reset_context, set_context_key 75 >>> get_context() 76 {} 77 >>> reset_token = set_context_key(key="key", value="value") 78 >>> get_context() 79 {'key': 'value'} 80 >>> reset_context(token=reset_token) 81 >>> get_context() 82 {} 83 84 Args: 85 token (Token[dict[str, object]]): The reset Token. 86 """ 87 _context.reset(token)
Reset the logging context with the given token.
This is an advanced utility function which is not necessary for most use-cases.
The given token should be obtained from a set_context_key call and should never be
constructed from scratch manually.
Example usage:
>>> from contextuallogging import get_context, reset_context, set_context_key
>>> get_context()
{}
>>> reset_token = set_context_key(key="key", value="value")
>>> get_context()
{'key': 'value'}
>>> reset_context(token=reset_token)
>>> get_context()
{}
Arguments:
- token (Token[dict[str, object]]): The reset Token.