python-json-logger 3.0.1__py3-none-any.whl → 3.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_json_logger-3.2.0.dist-info/METADATA +84 -0
- python_json_logger-3.2.0.dist-info/NOTICE +5 -0
- python_json_logger-3.2.0.dist-info/RECORD +15 -0
- {python_json_logger-3.0.1.dist-info → python_json_logger-3.2.0.dist-info}/WHEEL +1 -1
- pythonjsonlogger/__init__.py +29 -0
- pythonjsonlogger/core.py +370 -0
- pythonjsonlogger/defaults.py +241 -0
- pythonjsonlogger/exception.py +27 -0
- pythonjsonlogger/json.py +119 -0
- pythonjsonlogger/msgspec.py +63 -0
- pythonjsonlogger/orjson.py +71 -0
- pythonjsonlogger/utils.py +40 -0
- python_json_logger-3.0.1.dist-info/METADATA +0 -234
- python_json_logger-3.0.1.dist-info/RECORD +0 -8
- pythonjsonlogger/jsonlogger.py +0 -304
- {python_json_logger-3.0.1.dist-info → python_json_logger-3.2.0.dist-info}/LICENSE +0 -0
- {python_json_logger-3.0.1.dist-info → python_json_logger-3.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-json-logger
|
|
3
|
+
Version: 3.2.0
|
|
4
|
+
Summary: JSON Log Formatter for the Python Logging Package
|
|
5
|
+
Author-email: Zakaria Zajac <zak@madzak.com>, Nicholas Hairs <info+python-json-logger@nicholashairs.com>
|
|
6
|
+
Maintainer-email: Nicholas Hairs <info+python-json-logger@nicholashairs.com>
|
|
7
|
+
License: BSD-2-Clause License
|
|
8
|
+
Project-URL: Homepage, https://nhairs.github.io/python-json-logger
|
|
9
|
+
Project-URL: GitHub, https://github.com/nhairs/python-json-logger
|
|
10
|
+
Classifier: Development Status :: 6 - Mature
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: System :: Logging
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
License-File: NOTICE
|
|
27
|
+
Requires-Dist: typing_extensions; python_version < "3.10"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: orjson; implementation_name != "pypy" and extra == "dev"
|
|
30
|
+
Requires-Dist: msgspec; (implementation_name != "pypy" and python_version < "3.13") and extra == "dev"
|
|
31
|
+
Requires-Dist: msgspec-python313-pre; (implementation_name != "pypy" and python_version == "3.13") and extra == "dev"
|
|
32
|
+
Requires-Dist: validate-pyproject[all]; extra == "dev"
|
|
33
|
+
Requires-Dist: black; extra == "dev"
|
|
34
|
+
Requires-Dist: pylint; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest; extra == "dev"
|
|
37
|
+
Requires-Dist: freezegun; extra == "dev"
|
|
38
|
+
Requires-Dist: backports.zoneinfo; python_version < "3.9" and extra == "dev"
|
|
39
|
+
Requires-Dist: tzdata; extra == "dev"
|
|
40
|
+
Requires-Dist: build; extra == "dev"
|
|
41
|
+
Requires-Dist: mkdocs; extra == "dev"
|
|
42
|
+
Requires-Dist: mkdocs-material>=8.5; extra == "dev"
|
|
43
|
+
Requires-Dist: mkdocs-awesome-pages-plugin; extra == "dev"
|
|
44
|
+
Requires-Dist: mdx_truly_sane_lists; extra == "dev"
|
|
45
|
+
Requires-Dist: mkdocstrings[python]; extra == "dev"
|
|
46
|
+
Requires-Dist: mkdocs-gen-files; extra == "dev"
|
|
47
|
+
Requires-Dist: mkdocs-literate-nav; extra == "dev"
|
|
48
|
+
Requires-Dist: mike; extra == "dev"
|
|
49
|
+
|
|
50
|
+
<!-- [](https://pypi.python.org/pypi/python-json-logger/)
|
|
51
|
+
[](https://pypi.python.org/pypi/python-json-logger/)
|
|
52
|
+
[](https://github.com/nhairs/python-json-logger) -->
|
|
53
|
+
[](https://github.com/nhairs/python-json-logger)
|
|
54
|
+

|
|
55
|
+
#
|
|
56
|
+
# Python JSON Logger
|
|
57
|
+
|
|
58
|
+
Python JSON Logger enables you produce JSON logs when using Python's `logging` package.
|
|
59
|
+
|
|
60
|
+
JSON logs are machine readable allowing for much easier parsing and ingestion into log aggregation tools.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### 🚨 Important 🚨
|
|
64
|
+
|
|
65
|
+
This repository is a maintained fork of [madzak/python-json-logger](https://github.com/madzak/python-json-logger) pending [a PEP 541 request](https://github.com/pypi/support/issues/3607) for the PyPI package. The future direction of the project is being discussed [here](https://github.com/nhairs/python-json-logger/issues/1).
|
|
66
|
+
|
|
67
|
+
## Documentation
|
|
68
|
+
|
|
69
|
+
- [Documentation](https://nhairs.github.io/python-json-logger/latest/)
|
|
70
|
+
- [Quickstart Guide](https://nhairs.github.io/python-json-logger/latest/quickstart/)
|
|
71
|
+
- [Change Log](https://nhairs.github.io/python-json-logger/latest/changelog/)
|
|
72
|
+
- [Contributing](https://nhairs.github.io/python-json-logger/latest/contributing/)
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
This project is licensed under the BSD 2 Clause License - see [`LICENSE`](https://github.com/nhairs/python-json-logger/blob/main/LICENSE)
|
|
77
|
+
|
|
78
|
+
## Authors and Maintainers
|
|
79
|
+
|
|
80
|
+
This project was originally authored by [Zakaria Zajac](https://github.com/madzak) and our wonderful [contributors](https://github.com/nhairs/python-json-logger/graphs/contributors)
|
|
81
|
+
|
|
82
|
+
It is currently maintained by:
|
|
83
|
+
|
|
84
|
+
- [Nicholas Hairs](https://github.com/nhairs) - [nicholashairs.com](https://www.nicholashairs.com)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
pythonjsonlogger/__init__.py,sha256=mvnVMTGrh32otHxVE7-MsxT9kBvmMyS6I3NCQDe55LY,898
|
|
2
|
+
pythonjsonlogger/core.py,sha256=zrf2vBMPVjPv5ornwWs2TOi68FbWDnuhj_DrcUiuBG0,13328
|
|
3
|
+
pythonjsonlogger/defaults.py,sha256=-XgxIj8ioq7CsnBBMsQhTRpU-lKbLrpQLJTCaT3iH38,6577
|
|
4
|
+
pythonjsonlogger/exception.py,sha256=r3DXDk7TThscnMVNPpVRaRSFHTnY-ygea6nn-FgjwsI,804
|
|
5
|
+
pythonjsonlogger/json.py,sha256=TsKD_1-TDjaeMUg6la_aVzIWd7GBxgAntY6zWzVe7lU,4165
|
|
6
|
+
pythonjsonlogger/msgspec.py,sha256=M5kiIX4RLr1PwvWKx21N8sgk05LCFymBAWM4cRR3VNA,2161
|
|
7
|
+
pythonjsonlogger/orjson.py,sha256=HVhIHo7CrTwj9pZuZzUmj1qsW0coNdVmDKSDycDs-pM,2357
|
|
8
|
+
pythonjsonlogger/py.typed,sha256=4RLptUHQuSqzK6CDbigff8uvWQjwPPQMxikWPkV4OtA,80
|
|
9
|
+
pythonjsonlogger/utils.py,sha256=qr04nb61TkVw7cJTXAtLBfyH_NBvyYUlR_mehH76-RM,1126
|
|
10
|
+
python_json_logger-3.2.0.dist-info/LICENSE,sha256=GOqVF546Xg4k62zhbED7--IxM8JQKTOi91-KPhoFW1Q,1329
|
|
11
|
+
python_json_logger-3.2.0.dist-info/METADATA,sha256=5eZGCDrb3mmrvVyiug9EYBtzaD_PDzZmHw_UUy4dlKc,4357
|
|
12
|
+
python_json_logger-3.2.0.dist-info/NOTICE,sha256=uRQnFcGZCwuIBR2AIHVvhKi9w1KkiI_vAyralfxCsk4,209
|
|
13
|
+
python_json_logger-3.2.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
14
|
+
python_json_logger-3.2.0.dist-info/top_level.txt,sha256=9G-OsTkbwPgM8t-bXKPmWDBRZv9cbzLIiEDE5EnGk2I,17
|
|
15
|
+
python_json_logger-3.2.0.dist-info/RECORD,,
|
pythonjsonlogger/__init__.py
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
### IMPORTS
|
|
2
|
+
### ============================================================================
|
|
3
|
+
## Future
|
|
4
|
+
|
|
5
|
+
## Standard Library
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
## Installed
|
|
9
|
+
|
|
10
|
+
## Application
|
|
11
|
+
import pythonjsonlogger.json
|
|
12
|
+
import pythonjsonlogger.utils
|
|
13
|
+
|
|
14
|
+
### CONSTANTS
|
|
15
|
+
### ============================================================================
|
|
16
|
+
ORJSON_AVAILABLE = pythonjsonlogger.utils.package_is_available("orjson")
|
|
17
|
+
MSGSPEC_AVAILABLE = pythonjsonlogger.utils.package_is_available("msgspec")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### DEPRECATED COMPATIBILITY
|
|
21
|
+
### ============================================================================
|
|
22
|
+
def __getattr__(name: str):
|
|
23
|
+
if name == "jsonlogger":
|
|
24
|
+
warnings.warn(
|
|
25
|
+
"pythonjsonlogger.jsonlogger has been moved to pythonjsonlogger.json",
|
|
26
|
+
DeprecationWarning,
|
|
27
|
+
)
|
|
28
|
+
return pythonjsonlogger.json
|
|
29
|
+
raise AttributeError(f"module {__name__} has no attribute {name}")
|
pythonjsonlogger/core.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Core functionality shared by all JSON loggers"""
|
|
2
|
+
|
|
3
|
+
### IMPORTS
|
|
4
|
+
### ============================================================================
|
|
5
|
+
## Future
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
## Standard Library
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
import importlib
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Optional, Union, Callable, List, Dict, Container, Any, Sequence
|
|
15
|
+
|
|
16
|
+
if sys.version_info >= (3, 10):
|
|
17
|
+
from typing import TypeAlias
|
|
18
|
+
else:
|
|
19
|
+
from typing_extensions import TypeAlias
|
|
20
|
+
|
|
21
|
+
## Installed
|
|
22
|
+
|
|
23
|
+
## Application
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### CONSTANTS
|
|
27
|
+
### ============================================================================
|
|
28
|
+
RESERVED_ATTRS: List[str] = [
|
|
29
|
+
"args",
|
|
30
|
+
"asctime",
|
|
31
|
+
"created",
|
|
32
|
+
"exc_info",
|
|
33
|
+
"exc_text",
|
|
34
|
+
"filename",
|
|
35
|
+
"funcName",
|
|
36
|
+
"levelname",
|
|
37
|
+
"levelno",
|
|
38
|
+
"lineno",
|
|
39
|
+
"module",
|
|
40
|
+
"msecs",
|
|
41
|
+
"message",
|
|
42
|
+
"msg",
|
|
43
|
+
"name",
|
|
44
|
+
"pathname",
|
|
45
|
+
"process",
|
|
46
|
+
"processName",
|
|
47
|
+
"relativeCreated",
|
|
48
|
+
"stack_info",
|
|
49
|
+
"thread",
|
|
50
|
+
"threadName",
|
|
51
|
+
]
|
|
52
|
+
"""Default reserved attributes.
|
|
53
|
+
|
|
54
|
+
These come from the [default attributes of `LogRecord` objects](http://docs.python.org/library/logging.html#logrecord-attributes).
|
|
55
|
+
|
|
56
|
+
Note:
|
|
57
|
+
Although considered a constant, this list is dependent on the Python version due to
|
|
58
|
+
different `LogRecord` objects having different attributes in different Python versions.
|
|
59
|
+
|
|
60
|
+
*Changed in 3.0*: `RESERVED_ATTRS` is now `list[str]` instead of `tuple[str, ...]`.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
if sys.version_info >= (3, 12):
|
|
64
|
+
# taskName added in python 3.12
|
|
65
|
+
RESERVED_ATTRS.append("taskName")
|
|
66
|
+
RESERVED_ATTRS.sort()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
STYLE_STRING_TEMPLATE_REGEX = re.compile(r"\$\{(.+?)\}", re.IGNORECASE) # $ style
|
|
70
|
+
STYLE_STRING_FORMAT_REGEX = re.compile(r"\{(.+?)\}", re.IGNORECASE) # { style
|
|
71
|
+
STYLE_PERCENT_REGEX = re.compile(r"%\((.+?)\)", re.IGNORECASE) # % style
|
|
72
|
+
|
|
73
|
+
## Type Aliases
|
|
74
|
+
## -----------------------------------------------------------------------------
|
|
75
|
+
OptionalCallableOrStr: TypeAlias = Optional[Union[Callable, str]]
|
|
76
|
+
"""Type alias"""
|
|
77
|
+
|
|
78
|
+
LogRecord: TypeAlias = Dict[str, Any]
|
|
79
|
+
"""Type alias"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
### FUNCTIONS
|
|
83
|
+
### ============================================================================
|
|
84
|
+
def str_to_object(obj: Any) -> Any:
|
|
85
|
+
"""Import strings to an object, leaving non-strings as-is.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
obj: the object or string to process
|
|
89
|
+
|
|
90
|
+
*New in 3.1*
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
if not isinstance(obj, str):
|
|
94
|
+
return obj
|
|
95
|
+
|
|
96
|
+
module_name, attribute_name = obj.rsplit(".", 1)
|
|
97
|
+
return getattr(importlib.import_module(module_name), attribute_name)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def merge_record_extra(
|
|
101
|
+
record: logging.LogRecord,
|
|
102
|
+
target: Dict,
|
|
103
|
+
reserved: Container[str],
|
|
104
|
+
rename_fields: Optional[Dict[str, str]] = None,
|
|
105
|
+
) -> Dict:
|
|
106
|
+
"""
|
|
107
|
+
Merges extra attributes from LogRecord object into target dictionary
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
record: logging.LogRecord
|
|
111
|
+
target: dict to update
|
|
112
|
+
reserved: dict or list with reserved keys to skip
|
|
113
|
+
rename_fields: an optional dict, used to rename field names in the output.
|
|
114
|
+
e.g. Rename `levelname` to `log.level`: `{'levelname': 'log.level'}`
|
|
115
|
+
|
|
116
|
+
*Changed in 3.1*: `reserved` is now `Container[str]`.
|
|
117
|
+
"""
|
|
118
|
+
if rename_fields is None:
|
|
119
|
+
rename_fields = {}
|
|
120
|
+
for key, value in record.__dict__.items():
|
|
121
|
+
# this allows to have numeric keys
|
|
122
|
+
if key not in reserved and not (hasattr(key, "startswith") and key.startswith("_")):
|
|
123
|
+
target[rename_fields.get(key, key)] = value
|
|
124
|
+
return target
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
### CLASSES
|
|
128
|
+
### ============================================================================
|
|
129
|
+
class BaseJsonFormatter(logging.Formatter):
|
|
130
|
+
"""Base class for all formatters
|
|
131
|
+
|
|
132
|
+
Must not be used directly.
|
|
133
|
+
|
|
134
|
+
*New in 3.1*
|
|
135
|
+
|
|
136
|
+
*Changed in 3.2*: `defaults` argument is no longer ignored.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
_style: Union[logging.PercentStyle, str] # type: ignore[assignment]
|
|
140
|
+
|
|
141
|
+
## Parent Methods
|
|
142
|
+
## -------------------------------------------------------------------------
|
|
143
|
+
# pylint: disable=too-many-arguments,super-init-not-called
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
fmt: Optional[str] = None,
|
|
147
|
+
datefmt: Optional[str] = None,
|
|
148
|
+
style: str = "%",
|
|
149
|
+
validate: bool = True,
|
|
150
|
+
*,
|
|
151
|
+
prefix: str = "",
|
|
152
|
+
rename_fields: Optional[Dict[str, str]] = None,
|
|
153
|
+
rename_fields_keep_missing: bool = False,
|
|
154
|
+
static_fields: Optional[Dict[str, Any]] = None,
|
|
155
|
+
reserved_attrs: Optional[Sequence[str]] = None,
|
|
156
|
+
timestamp: Union[bool, str] = False,
|
|
157
|
+
defaults: Optional[Dict[str, Any]] = None,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Args:
|
|
161
|
+
fmt: string representing fields to log
|
|
162
|
+
datefmt: format to use when formatting `asctime` field
|
|
163
|
+
style: how to extract log fields from `fmt`
|
|
164
|
+
validate: validate `fmt` against style, if implementing a custom `style` you
|
|
165
|
+
must set this to `False`.
|
|
166
|
+
defaults: a dictionary containing default fields that are added before all other fields and
|
|
167
|
+
may be overridden. The supplied fields are still subject to `rename_fields`.
|
|
168
|
+
prefix: an optional string prefix added at the beginning of
|
|
169
|
+
the formatted string
|
|
170
|
+
rename_fields: an optional dict, used to rename field names in the output.
|
|
171
|
+
Rename `message` to `@message`: `{'message': '@message'}`
|
|
172
|
+
rename_fields_keep_missing: When renaming fields, include missing fields in the output.
|
|
173
|
+
static_fields: an optional dict, used to add fields with static values to all logs
|
|
174
|
+
reserved_attrs: an optional list of fields that will be skipped when
|
|
175
|
+
outputting json log record. Defaults to [all log record attributes][pythonjsonlogger.core.RESERVED_ATTRS].
|
|
176
|
+
timestamp: an optional string/boolean field to add a timestamp when
|
|
177
|
+
outputting the json log record. If string is passed, timestamp will be added
|
|
178
|
+
to log record using string as key. If True boolean is passed, timestamp key
|
|
179
|
+
will be "timestamp". Defaults to False/off.
|
|
180
|
+
|
|
181
|
+
*Changed in 3.1*:
|
|
182
|
+
|
|
183
|
+
- you can now use custom values for style by setting validate to `False`.
|
|
184
|
+
The value is stored in `self._style` as a string. The `parse` method will need to be
|
|
185
|
+
overridden in order to support the new style.
|
|
186
|
+
- Renaming fields now preserves the order that fields were added in and avoids adding
|
|
187
|
+
missing fields. The original behaviour, missing fields have a value of `None`, is still
|
|
188
|
+
available by setting `rename_fields_keep_missing` to `True`.
|
|
189
|
+
"""
|
|
190
|
+
## logging.Formatter compatibility
|
|
191
|
+
## ---------------------------------------------------------------------
|
|
192
|
+
# Note: validate added in 3.8, defaults added in 3.10
|
|
193
|
+
if style in logging._STYLES:
|
|
194
|
+
_style = logging._STYLES[style][0](fmt) # type: ignore[operator]
|
|
195
|
+
if validate:
|
|
196
|
+
_style.validate()
|
|
197
|
+
self._style = _style
|
|
198
|
+
self._fmt = _style._fmt
|
|
199
|
+
|
|
200
|
+
elif not validate:
|
|
201
|
+
self._style = style
|
|
202
|
+
self._fmt = fmt
|
|
203
|
+
|
|
204
|
+
else:
|
|
205
|
+
raise ValueError(f"Style must be one of: {','.join(logging._STYLES.keys())}")
|
|
206
|
+
|
|
207
|
+
self.datefmt = datefmt
|
|
208
|
+
|
|
209
|
+
## JSON Logging specific
|
|
210
|
+
## ---------------------------------------------------------------------
|
|
211
|
+
self.prefix = prefix
|
|
212
|
+
self.rename_fields = rename_fields if rename_fields is not None else {}
|
|
213
|
+
self.rename_fields_keep_missing = rename_fields_keep_missing
|
|
214
|
+
self.static_fields = static_fields if static_fields is not None else {}
|
|
215
|
+
self.reserved_attrs = set(reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS)
|
|
216
|
+
self.timestamp = timestamp
|
|
217
|
+
|
|
218
|
+
self._required_fields = self.parse()
|
|
219
|
+
self._skip_fields = set(self._required_fields)
|
|
220
|
+
self._skip_fields.update(self.reserved_attrs)
|
|
221
|
+
self.defaults = defaults if defaults is not None else {}
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
225
|
+
"""Formats a log record and serializes to json
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
record: the record to format
|
|
229
|
+
"""
|
|
230
|
+
message_dict: Dict[str, Any] = {}
|
|
231
|
+
# TODO: logging.LogRecord.msg and logging.LogRecord.message in typeshed
|
|
232
|
+
# are always type of str. We shouldn't need to override that.
|
|
233
|
+
if isinstance(record.msg, dict):
|
|
234
|
+
message_dict = record.msg
|
|
235
|
+
record.message = ""
|
|
236
|
+
else:
|
|
237
|
+
record.message = record.getMessage()
|
|
238
|
+
|
|
239
|
+
# only format time if needed
|
|
240
|
+
if "asctime" in self._required_fields:
|
|
241
|
+
record.asctime = self.formatTime(record, self.datefmt)
|
|
242
|
+
|
|
243
|
+
# Display formatted exception, but allow overriding it in the
|
|
244
|
+
# user-supplied dict.
|
|
245
|
+
if record.exc_info and not message_dict.get("exc_info"):
|
|
246
|
+
message_dict["exc_info"] = self.formatException(record.exc_info)
|
|
247
|
+
if not message_dict.get("exc_info") and record.exc_text:
|
|
248
|
+
message_dict["exc_info"] = record.exc_text
|
|
249
|
+
|
|
250
|
+
# Display formatted record of stack frames
|
|
251
|
+
# default format is a string returned from :func:`traceback.print_stack`
|
|
252
|
+
if record.stack_info and not message_dict.get("stack_info"):
|
|
253
|
+
message_dict["stack_info"] = self.formatStack(record.stack_info)
|
|
254
|
+
|
|
255
|
+
log_record: LogRecord = {}
|
|
256
|
+
self.add_fields(log_record, record, message_dict)
|
|
257
|
+
log_record = self.process_log_record(log_record)
|
|
258
|
+
|
|
259
|
+
return self.serialize_log_record(log_record)
|
|
260
|
+
|
|
261
|
+
## JSON Formatter Specific Methods
|
|
262
|
+
## -------------------------------------------------------------------------
|
|
263
|
+
def parse(self) -> List[str]:
|
|
264
|
+
"""Parses format string looking for substitutions
|
|
265
|
+
|
|
266
|
+
This method is responsible for returning a list of fields (as strings)
|
|
267
|
+
to include in all log messages.
|
|
268
|
+
|
|
269
|
+
You can support custom styles by overriding this method.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
list of fields to be extracted and serialized
|
|
273
|
+
"""
|
|
274
|
+
if isinstance(self._style, logging.StringTemplateStyle):
|
|
275
|
+
formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX
|
|
276
|
+
|
|
277
|
+
elif isinstance(self._style, logging.StrFormatStyle):
|
|
278
|
+
formatter_style_pattern = STYLE_STRING_FORMAT_REGEX
|
|
279
|
+
|
|
280
|
+
elif isinstance(self._style, logging.PercentStyle):
|
|
281
|
+
# PercentStyle is parent class of StringTemplateStyle and StrFormatStyle
|
|
282
|
+
# so it must be checked last.
|
|
283
|
+
formatter_style_pattern = STYLE_PERCENT_REGEX
|
|
284
|
+
|
|
285
|
+
else:
|
|
286
|
+
raise ValueError(f"Style {self._style!r} is not supported")
|
|
287
|
+
|
|
288
|
+
if self._fmt:
|
|
289
|
+
return formatter_style_pattern.findall(self._fmt)
|
|
290
|
+
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
def serialize_log_record(self, log_record: LogRecord) -> str:
|
|
294
|
+
"""Returns the final representation of the log record.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
log_record: the log record
|
|
298
|
+
"""
|
|
299
|
+
return self.prefix + self.jsonify_log_record(log_record)
|
|
300
|
+
|
|
301
|
+
def add_fields(
|
|
302
|
+
self,
|
|
303
|
+
log_record: Dict[str, Any],
|
|
304
|
+
record: logging.LogRecord,
|
|
305
|
+
message_dict: Dict[str, Any],
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Extract fields from a LogRecord for logging
|
|
308
|
+
|
|
309
|
+
This method can be overridden to implement custom logic for adding fields.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
log_record: data that will be logged
|
|
313
|
+
record: the record to extract data from
|
|
314
|
+
message_dict: dictionary that was logged instead of a message. e.g
|
|
315
|
+
`logger.info({"is_this_message_dict": True})`
|
|
316
|
+
"""
|
|
317
|
+
for field in self.defaults:
|
|
318
|
+
log_record[self._get_rename(field)] = self.defaults[field]
|
|
319
|
+
|
|
320
|
+
for field in self._required_fields:
|
|
321
|
+
log_record[self._get_rename(field)] = record.__dict__.get(field)
|
|
322
|
+
|
|
323
|
+
for data_dict in [self.static_fields, message_dict]:
|
|
324
|
+
for key, value in data_dict.items():
|
|
325
|
+
log_record[self._get_rename(key)] = value
|
|
326
|
+
|
|
327
|
+
merge_record_extra(
|
|
328
|
+
record,
|
|
329
|
+
log_record,
|
|
330
|
+
reserved=self._skip_fields,
|
|
331
|
+
rename_fields=self.rename_fields,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if self.timestamp:
|
|
335
|
+
key = self.timestamp if isinstance(self.timestamp, str) else "timestamp"
|
|
336
|
+
log_record[self._get_rename(key)] = datetime.fromtimestamp(
|
|
337
|
+
record.created, tz=timezone.utc
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if self.rename_fields_keep_missing:
|
|
341
|
+
for field in self.rename_fields.values():
|
|
342
|
+
if field not in log_record:
|
|
343
|
+
log_record[field] = None
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
def _get_rename(self, key: str) -> str:
|
|
347
|
+
return self.rename_fields.get(key, key)
|
|
348
|
+
|
|
349
|
+
# Child Methods
|
|
350
|
+
# ..........................................................................
|
|
351
|
+
def jsonify_log_record(self, log_record: LogRecord) -> str:
|
|
352
|
+
"""Convert this log record into a JSON string.
|
|
353
|
+
|
|
354
|
+
Child classes MUST override this method.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
log_record: the data to serialize
|
|
358
|
+
"""
|
|
359
|
+
raise NotImplementedError()
|
|
360
|
+
|
|
361
|
+
def process_log_record(self, log_record: LogRecord) -> LogRecord:
|
|
362
|
+
"""Custom processing of the log record.
|
|
363
|
+
|
|
364
|
+
Child classes can override this method to alter the log record before it
|
|
365
|
+
is serialized.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
log_record: incoming data
|
|
369
|
+
"""
|
|
370
|
+
return log_record
|