about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/pythonjsonlogger/core.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/pythonjsonlogger/core.py')
-rw-r--r--.venv/lib/python3.12/site-packages/pythonjsonlogger/core.py394
1 files changed, 394 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pythonjsonlogger/core.py b/.venv/lib/python3.12/site-packages/pythonjsonlogger/core.py
new file mode 100644
index 00000000..1a4dee38
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pythonjsonlogger/core.py
@@ -0,0 +1,394 @@
+"""Core functionality shared by all JSON loggers"""
+
+### IMPORTS
+### ============================================================================
+## Future
+from __future__ import annotations
+
+## Standard Library
+from datetime import datetime, timezone
+import importlib
+import logging
+import re
+import sys
+from typing import Optional, Union, Callable, List, Dict, Container, Any, Sequence
+
+if sys.version_info >= (3, 10):
+    from typing import TypeAlias
+else:
+    from typing_extensions import TypeAlias
+
+## Installed
+
+## Application
+
+
+### CONSTANTS
+### ============================================================================
+RESERVED_ATTRS: List[str] = [
+    "args",
+    "asctime",
+    "created",
+    "exc_info",
+    "exc_text",
+    "filename",
+    "funcName",
+    "levelname",
+    "levelno",
+    "lineno",
+    "module",
+    "msecs",
+    "message",
+    "msg",
+    "name",
+    "pathname",
+    "process",
+    "processName",
+    "relativeCreated",
+    "stack_info",
+    "thread",
+    "threadName",
+]
+"""Default reserved attributes.
+
+These come from the [default attributes of `LogRecord` objects](http://docs.python.org/library/logging.html#logrecord-attributes).
+
+Note:
+    Although considered a constant, this list is dependent on the Python version due to
+    different `LogRecord` objects having different attributes in different Python versions.
+
+*Changed in 3.0*: `RESERVED_ATTRS` is now `list[str]` instead of `tuple[str, ...]`.
+"""
+
+if sys.version_info >= (3, 12):
+    # taskName added in python 3.12
+    RESERVED_ATTRS.append("taskName")
+    RESERVED_ATTRS.sort()
+
+
+STYLE_STRING_TEMPLATE_REGEX = re.compile(r"\$\{(.+?)\}", re.IGNORECASE)  # $ style
+STYLE_STRING_FORMAT_REGEX = re.compile(r"\{(.+?)\}", re.IGNORECASE)  # { style
+STYLE_PERCENT_REGEX = re.compile(r"%\((.+?)\)", re.IGNORECASE)  # % style
+
+## Type Aliases
+## -----------------------------------------------------------------------------
+OptionalCallableOrStr: TypeAlias = Optional[Union[Callable, str]]
+"""Type alias"""
+
+LogRecord: TypeAlias = Dict[str, Any]
+"""Type alias"""
+
+
+### FUNCTIONS
+### ============================================================================
+def str_to_object(obj: Any) -> Any:
+    """Import strings to an object, leaving non-strings as-is.
+
+    Args:
+        obj: the object or string to process
+
+    *New in 3.1*
+    """
+
+    if not isinstance(obj, str):
+        return obj
+
+    module_name, attribute_name = obj.rsplit(".", 1)
+    return getattr(importlib.import_module(module_name), attribute_name)
+
+
+def merge_record_extra(
+    record: logging.LogRecord,
+    target: Dict,
+    reserved: Container[str],
+    rename_fields: Optional[Dict[str, str]] = None,
+) -> Dict:
+    """
+    Merges extra attributes from LogRecord object into target dictionary
+
+    Args:
+        record: logging.LogRecord
+        target: dict to update
+        reserved: dict or list with reserved keys to skip
+        rename_fields: an optional dict, used to rename field names in the output.
+            e.g. Rename `levelname` to `log.level`: `{'levelname': 'log.level'}`
+
+    *Changed in 3.1*: `reserved` is now `Container[str]`.
+    """
+    if rename_fields is None:
+        rename_fields = {}
+    for key, value in record.__dict__.items():
+        # this allows to have numeric keys
+        if key not in reserved and not (hasattr(key, "startswith") and key.startswith("_")):
+            target[rename_fields.get(key, key)] = value
+    return target
+
+
+### CLASSES
+### ============================================================================
+class BaseJsonFormatter(logging.Formatter):
+    """Base class for all formatters
+
+    Must not be used directly.
+
+    *New in 3.1*
+
+    *Changed in 3.2*: `defaults` argument is no longer ignored.
+
+    *Added in UNRELEASED*: `exc_info_as_array` and `stack_info_as_array` options are added.
+    """
+
+    _style: Union[logging.PercentStyle, str]  # type: ignore[assignment]
+
+    ## Parent Methods
+    ## -------------------------------------------------------------------------
+    # pylint: disable=too-many-arguments,super-init-not-called
+    def __init__(
+        self,
+        fmt: Optional[str] = None,
+        datefmt: Optional[str] = None,
+        style: str = "%",
+        validate: bool = True,
+        *,
+        prefix: str = "",
+        rename_fields: Optional[Dict[str, str]] = None,
+        rename_fields_keep_missing: bool = False,
+        static_fields: Optional[Dict[str, Any]] = None,
+        reserved_attrs: Optional[Sequence[str]] = None,
+        timestamp: Union[bool, str] = False,
+        defaults: Optional[Dict[str, Any]] = None,
+        exc_info_as_array: bool = False,
+        stack_info_as_array: bool = False,
+    ) -> None:
+        """
+        Args:
+            fmt: string representing fields to log
+            datefmt: format to use when formatting `asctime` field
+            style: how to extract log fields from `fmt`
+            validate: validate `fmt` against style, if implementing a custom `style` you
+                must set this to `False`.
+            defaults: a dictionary containing default fields that are added before all other fields and
+                may be overridden. The supplied fields are still subject to `rename_fields`.
+            prefix: an optional string prefix added at the beginning of
+                the formatted string
+            rename_fields: an optional dict, used to rename field names in the output.
+                Rename `message` to `@message`: `{'message': '@message'}`
+            rename_fields_keep_missing: When renaming fields, include missing fields in the output.
+            static_fields: an optional dict, used to add fields with static values to all logs
+            reserved_attrs: an optional list of fields that will be skipped when
+                outputting json log record. Defaults to [all log record attributes][pythonjsonlogger.core.RESERVED_ATTRS].
+            timestamp: an optional string/boolean field to add a timestamp when
+                outputting the json log record. If string is passed, timestamp will be added
+                to log record using string as key. If True boolean is passed, timestamp key
+                will be "timestamp". Defaults to False/off.
+            exc_info_as_array: break the exc_info into a list of lines based on line breaks.
+            stack_info_as_array: break the stack_info into a list of lines based on line breaks.
+
+        *Changed in 3.1*:
+
+        - you can now use custom values for style by setting validate to `False`.
+          The value is stored in `self._style` as a string. The `parse` method will need to be
+          overridden in order to support the new style.
+        - Renaming fields now preserves the order that fields were added in and avoids adding
+          missing fields. The original behaviour, missing fields have a value of `None`, is still
+          available by setting `rename_fields_keep_missing` to `True`.
+        """
+        ## logging.Formatter compatibility
+        ## ---------------------------------------------------------------------
+        # Note: validate added in 3.8, defaults added in 3.10
+        if style in logging._STYLES:
+            _style = logging._STYLES[style][0](fmt)  # type: ignore[operator]
+            if validate:
+                _style.validate()
+            self._style = _style
+            self._fmt = _style._fmt
+
+        elif not validate:
+            self._style = style
+            self._fmt = fmt
+
+        else:
+            raise ValueError(f"Style must be one of: {','.join(logging._STYLES.keys())}")
+
+        self.datefmt = datefmt
+
+        ## JSON Logging specific
+        ## ---------------------------------------------------------------------
+        self.prefix = prefix
+        self.rename_fields = rename_fields if rename_fields is not None else {}
+        self.rename_fields_keep_missing = rename_fields_keep_missing
+        self.static_fields = static_fields if static_fields is not None else {}
+        self.reserved_attrs = set(reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS)
+        self.timestamp = timestamp
+
+        self._required_fields = self.parse()
+        self._skip_fields = set(self._required_fields)
+        self._skip_fields.update(self.reserved_attrs)
+        self.defaults = defaults if defaults is not None else {}
+        self.exc_info_as_array = exc_info_as_array
+        self.stack_info_as_array = stack_info_as_array
+        return
+
+    def format(self, record: logging.LogRecord) -> str:
+        """Formats a log record and serializes to json
+
+        Args:
+            record: the record to format
+        """
+        message_dict: Dict[str, Any] = {}
+        # TODO: logging.LogRecord.msg and logging.LogRecord.message in typeshed
+        #        are always type of str. We shouldn't need to override that.
+        if isinstance(record.msg, dict):
+            message_dict = record.msg
+            record.message = ""
+        else:
+            record.message = record.getMessage()
+
+        # only format time if needed
+        if "asctime" in self._required_fields:
+            record.asctime = self.formatTime(record, self.datefmt)
+
+        # Display formatted exception, but allow overriding it in the
+        # user-supplied dict.
+        if record.exc_info and not message_dict.get("exc_info"):
+            message_dict["exc_info"] = self.formatException(record.exc_info)
+        if not message_dict.get("exc_info") and record.exc_text:
+            message_dict["exc_info"] = record.exc_text
+
+        # Display formatted record of stack frames
+        # default format is a string returned from :func:`traceback.print_stack`
+        if record.stack_info and not message_dict.get("stack_info"):
+            message_dict["stack_info"] = self.formatStack(record.stack_info)
+
+        log_record: LogRecord = {}
+        self.add_fields(log_record, record, message_dict)
+        log_record = self.process_log_record(log_record)
+
+        return self.serialize_log_record(log_record)
+
+    ## JSON Formatter Specific Methods
+    ## -------------------------------------------------------------------------
+    def parse(self) -> List[str]:
+        """Parses format string looking for substitutions
+
+        This method is responsible for returning a list of fields (as strings)
+        to include in all log messages.
+
+        You can support custom styles by overriding this method.
+
+        Returns:
+            list of fields to be extracted and serialized
+        """
+        if isinstance(self._style, logging.StringTemplateStyle):
+            formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX
+
+        elif isinstance(self._style, logging.StrFormatStyle):
+            formatter_style_pattern = STYLE_STRING_FORMAT_REGEX
+
+        elif isinstance(self._style, logging.PercentStyle):
+            # PercentStyle is parent class of StringTemplateStyle and StrFormatStyle
+            # so it must be checked last.
+            formatter_style_pattern = STYLE_PERCENT_REGEX
+
+        else:
+            raise ValueError(f"Style {self._style!r} is not supported")
+
+        if self._fmt:
+            return formatter_style_pattern.findall(self._fmt)
+
+        return []
+
+    def serialize_log_record(self, log_record: LogRecord) -> str:
+        """Returns the final representation of the log record.
+
+        Args:
+            log_record: the log record
+        """
+        return self.prefix + self.jsonify_log_record(log_record)
+
+    def add_fields(
+        self,
+        log_record: Dict[str, Any],
+        record: logging.LogRecord,
+        message_dict: Dict[str, Any],
+    ) -> None:
+        """Extract fields from a LogRecord for logging
+
+        This method can be overridden to implement custom logic for adding fields.
+
+        Args:
+            log_record: data that will be logged
+            record: the record to extract data from
+            message_dict: dictionary that was logged instead of a message. e.g
+                `logger.info({"is_this_message_dict": True})`
+        """
+        for field in self.defaults:
+            log_record[self._get_rename(field)] = self.defaults[field]
+
+        for field in self._required_fields:
+            log_record[self._get_rename(field)] = record.__dict__.get(field)
+
+        for data_dict in [self.static_fields, message_dict]:
+            for key, value in data_dict.items():
+                log_record[self._get_rename(key)] = value
+
+        merge_record_extra(
+            record,
+            log_record,
+            reserved=self._skip_fields,
+            rename_fields=self.rename_fields,
+        )
+
+        if self.timestamp:
+            key = self.timestamp if isinstance(self.timestamp, str) else "timestamp"
+            log_record[self._get_rename(key)] = datetime.fromtimestamp(
+                record.created, tz=timezone.utc
+            )
+
+        if self.rename_fields_keep_missing:
+            for field in self.rename_fields.values():
+                if field not in log_record:
+                    log_record[field] = None
+        return
+
+    def _get_rename(self, key: str) -> str:
+        return self.rename_fields.get(key, key)
+
+    # Child Methods
+    # ..........................................................................
+    def jsonify_log_record(self, log_record: LogRecord) -> str:
+        """Convert this log record into a JSON string.
+
+        Child classes MUST override this method.
+
+        Args:
+            log_record: the data to serialize
+        """
+        raise NotImplementedError()
+
+    def process_log_record(self, log_record: LogRecord) -> LogRecord:
+        """Custom processing of the log record.
+
+        Child classes can override this method to alter the log record before it
+        is serialized.
+
+        Args:
+            log_record: incoming data
+        """
+        return log_record
+
+    def formatException(self, ei) -> Union[str, list[str]]:  # type: ignore
+        """Format and return the specified exception information.
+
+        If exc_info_as_array is set to True, This method returns an array of strings.
+        """
+        exception_info_str = super().formatException(ei)
+        return exception_info_str.splitlines() if self.exc_info_as_array else exception_info_str
+
+    def formatStack(self, stack_info) -> Union[str, list[str]]:  # type: ignore
+        """Format and return the specified stack information.
+
+        If stack_info_as_array is set to True, This method returns an array of strings.
+        """
+        stack_info_str = super().formatStack(stack_info)
+        return stack_info_str.splitlines() if self.stack_info_as_array else stack_info_str