python-json-logger 4.0.0.dev0__tar.gz → 4.1.0__tar.gz
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-4.0.0.dev0/src/python_json_logger.egg-info → python_json_logger-4.1.0}/PKG-INFO +4 -9
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/pyproject.toml +5 -10
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0/src/python_json_logger.egg-info}/PKG-INFO +4 -9
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/python_json_logger.egg-info/requires.txt +0 -6
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/core.py +103 -91
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/defaults.py +1 -7
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/json.py +12 -11
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/msgspec.py +6 -5
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/orjson.py +7 -6
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/tests/test_dictconfig.py +20 -3
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/tests/test_formatters.py +50 -16
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/LICENSE +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/MANIFEST.in +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/NOTICE +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/README.md +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/setup.cfg +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/python_json_logger.egg-info/SOURCES.txt +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/python_json_logger.egg-info/dependency_links.txt +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/python_json_logger.egg-info/top_level.txt +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/__init__.py +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/exception.py +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/jsonlogger.py +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/py.typed +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/utils.py +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/tests/__init__.py +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/tests/test_deprecation.py +0 -0
- {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/tests/test_missing.py +0 -0
{python_json_logger-4.0.0.dev0/src/python_json_logger.egg-info → python_json_logger-4.1.0}/PKG-INFO
RENAMED
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-json-logger
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: JSON Log Formatter for the Python Logging Package
|
|
5
5
|
Author-email: Zakaria Zajac <zak@madzak.com>, Nicholas Hairs <info+python-json-logger@nicholashairs.com>
|
|
6
6
|
Maintainer-email: Nicholas Hairs <info+python-json-logger@nicholashairs.com>
|
|
7
|
-
License: BSD-2-Clause
|
|
7
|
+
License-Expression: BSD-2-Clause
|
|
8
8
|
Project-URL: Homepage, https://nhairs.github.io/python-json-logger
|
|
9
9
|
Project-URL: GitHub, https://github.com/nhairs/python-json-logger
|
|
10
10
|
Classifier: Development Status :: 6 - Mature
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
13
12
|
Classifier: Operating System :: OS Independent
|
|
14
13
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
19
|
Classifier: Topic :: System :: Logging
|
|
22
20
|
Classifier: Typing :: Typed
|
|
23
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.10
|
|
24
22
|
Description-Content-Type: text/markdown
|
|
25
23
|
License-File: LICENSE
|
|
26
|
-
License-File: NOTICE
|
|
27
|
-
Requires-Dist: typing_extensions; python_version < "3.10"
|
|
28
24
|
Provides-Extra: dev
|
|
29
25
|
Requires-Dist: orjson; implementation_name != "pypy" and extra == "dev"
|
|
30
26
|
Requires-Dist: msgspec; implementation_name != "pypy" and extra == "dev"
|
|
@@ -34,7 +30,6 @@ Requires-Dist: pylint; extra == "dev"
|
|
|
34
30
|
Requires-Dist: mypy; extra == "dev"
|
|
35
31
|
Requires-Dist: pytest; extra == "dev"
|
|
36
32
|
Requires-Dist: freezegun; extra == "dev"
|
|
37
|
-
Requires-Dist: backports.zoneinfo; python_version < "3.9" and extra == "dev"
|
|
38
33
|
Requires-Dist: tzdata; extra == "dev"
|
|
39
34
|
Requires-Dist: build; extra == "dev"
|
|
40
35
|
Requires-Dist: mkdocs; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-json-logger"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.1.0"
|
|
8
8
|
description = "JSON Log Formatter for the Python Logging Package"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Zakaria Zajac", email = "zak@madzak.com"},
|
|
@@ -15,26 +15,22 @@ maintainers = [
|
|
|
15
15
|
]
|
|
16
16
|
|
|
17
17
|
# Dependency Information
|
|
18
|
-
requires-python = ">=3.
|
|
19
|
-
dependencies = [
|
|
20
|
-
"typing_extensions;python_version<'3.10'",
|
|
21
|
-
]
|
|
18
|
+
requires-python = ">=3.10"
|
|
22
19
|
|
|
23
20
|
# Extra information
|
|
24
21
|
readme = "README.md"
|
|
25
|
-
license =
|
|
22
|
+
license = "BSD-2-Clause"
|
|
23
|
+
license-files = ["LICENSE"]
|
|
26
24
|
classifiers = [
|
|
27
25
|
"Development Status :: 6 - Mature",
|
|
28
26
|
"Intended Audience :: Developers",
|
|
29
|
-
"License :: OSI Approved :: BSD License",
|
|
30
27
|
"Operating System :: OS Independent",
|
|
31
28
|
"Programming Language :: Python :: 3 :: Only",
|
|
32
|
-
"Programming Language :: Python :: 3.8",
|
|
33
|
-
"Programming Language :: Python :: 3.9",
|
|
34
29
|
"Programming Language :: Python :: 3.10",
|
|
35
30
|
"Programming Language :: Python :: 3.11",
|
|
36
31
|
"Programming Language :: Python :: 3.12",
|
|
37
32
|
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Programming Language :: Python :: 3.14",
|
|
38
34
|
"Topic :: System :: Logging",
|
|
39
35
|
"Typing :: Typed",
|
|
40
36
|
]
|
|
@@ -56,7 +52,6 @@ dev = [
|
|
|
56
52
|
## Test
|
|
57
53
|
"pytest",
|
|
58
54
|
"freezegun",
|
|
59
|
-
"backports.zoneinfo;python_version<'3.9'",
|
|
60
55
|
"tzdata",
|
|
61
56
|
## Build
|
|
62
57
|
"build",
|
{python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0/src/python_json_logger.egg-info}/PKG-INFO
RENAMED
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-json-logger
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: JSON Log Formatter for the Python Logging Package
|
|
5
5
|
Author-email: Zakaria Zajac <zak@madzak.com>, Nicholas Hairs <info+python-json-logger@nicholashairs.com>
|
|
6
6
|
Maintainer-email: Nicholas Hairs <info+python-json-logger@nicholashairs.com>
|
|
7
|
-
License: BSD-2-Clause
|
|
7
|
+
License-Expression: BSD-2-Clause
|
|
8
8
|
Project-URL: Homepage, https://nhairs.github.io/python-json-logger
|
|
9
9
|
Project-URL: GitHub, https://github.com/nhairs/python-json-logger
|
|
10
10
|
Classifier: Development Status :: 6 - Mature
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
13
12
|
Classifier: Operating System :: OS Independent
|
|
14
13
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
19
|
Classifier: Topic :: System :: Logging
|
|
22
20
|
Classifier: Typing :: Typed
|
|
23
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.10
|
|
24
22
|
Description-Content-Type: text/markdown
|
|
25
23
|
License-File: LICENSE
|
|
26
|
-
License-File: NOTICE
|
|
27
|
-
Requires-Dist: typing_extensions; python_version < "3.10"
|
|
28
24
|
Provides-Extra: dev
|
|
29
25
|
Requires-Dist: orjson; implementation_name != "pypy" and extra == "dev"
|
|
30
26
|
Requires-Dist: msgspec; implementation_name != "pypy" and extra == "dev"
|
|
@@ -34,7 +30,6 @@ Requires-Dist: pylint; extra == "dev"
|
|
|
34
30
|
Requires-Dist: mypy; extra == "dev"
|
|
35
31
|
Requires-Dist: pytest; extra == "dev"
|
|
36
32
|
Requires-Dist: freezegun; extra == "dev"
|
|
37
|
-
Requires-Dist: backports.zoneinfo; python_version < "3.9" and extra == "dev"
|
|
38
33
|
Requires-Dist: tzdata; extra == "dev"
|
|
39
34
|
Requires-Dist: build; extra == "dev"
|
|
40
35
|
Requires-Dist: mkdocs; extra == "dev"
|
|
@@ -7,16 +7,11 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
## Standard Library
|
|
9
9
|
from datetime import datetime, timezone
|
|
10
|
-
import importlib
|
|
11
10
|
import logging
|
|
12
11
|
import re
|
|
13
12
|
import sys
|
|
14
|
-
from typing import
|
|
15
|
-
|
|
16
|
-
if sys.version_info >= (3, 10):
|
|
17
|
-
from typing import TypeAlias
|
|
18
|
-
else:
|
|
19
|
-
from typing_extensions import TypeAlias
|
|
13
|
+
from typing import TypeAlias, Any
|
|
14
|
+
from collections.abc import Container, Sequence
|
|
20
15
|
|
|
21
16
|
## Installed
|
|
22
17
|
|
|
@@ -25,7 +20,7 @@ else:
|
|
|
25
20
|
|
|
26
21
|
### CONSTANTS
|
|
27
22
|
### ============================================================================
|
|
28
|
-
RESERVED_ATTRS:
|
|
23
|
+
RESERVED_ATTRS: list[str] = [
|
|
29
24
|
"args",
|
|
30
25
|
"asctime",
|
|
31
26
|
"created",
|
|
@@ -72,37 +67,21 @@ STYLE_PERCENT_REGEX = re.compile(r"%\((.+?)\)", re.IGNORECASE) # % style
|
|
|
72
67
|
|
|
73
68
|
## Type Aliases
|
|
74
69
|
## -----------------------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
"""Type alias
|
|
70
|
+
LogData: TypeAlias = dict[str, Any]
|
|
71
|
+
"""Type alias
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
"""
|
|
73
|
+
*Changed in 4.0*: renamed from `LogRecord` to `LogData`
|
|
74
|
+
"""
|
|
80
75
|
|
|
81
76
|
|
|
82
77
|
### FUNCTIONS
|
|
83
78
|
### ============================================================================
|
|
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
79
|
def merge_record_extra(
|
|
101
80
|
record: logging.LogRecord,
|
|
102
|
-
target:
|
|
81
|
+
target: dict[Any, Any],
|
|
103
82
|
reserved: Container[str],
|
|
104
|
-
rename_fields:
|
|
105
|
-
) ->
|
|
83
|
+
rename_fields: dict[str, str] | None = None,
|
|
84
|
+
) -> dict[Any, Any]:
|
|
106
85
|
"""
|
|
107
86
|
Merges extra attributes from LogRecord object into target dictionary
|
|
108
87
|
|
|
@@ -135,38 +114,38 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
135
114
|
|
|
136
115
|
*Changed in 3.2*: `defaults` argument is no longer ignored.
|
|
137
116
|
|
|
138
|
-
*Added in
|
|
117
|
+
*Added in 3.3*: `exc_info_as_array` and `stack_info_as_array` options are added.
|
|
139
118
|
"""
|
|
140
119
|
|
|
141
|
-
_style:
|
|
120
|
+
_style: logging.PercentStyle | str # type: ignore[assignment]
|
|
142
121
|
|
|
143
122
|
## Parent Methods
|
|
144
123
|
## -------------------------------------------------------------------------
|
|
145
124
|
# pylint: disable=too-many-arguments,super-init-not-called
|
|
146
125
|
def __init__(
|
|
147
126
|
self,
|
|
148
|
-
fmt:
|
|
149
|
-
datefmt:
|
|
127
|
+
fmt: str | Sequence[str] | None = None,
|
|
128
|
+
datefmt: str | None = None,
|
|
150
129
|
style: str = "%",
|
|
151
130
|
validate: bool = True,
|
|
152
131
|
*,
|
|
153
132
|
prefix: str = "",
|
|
154
|
-
rename_fields:
|
|
133
|
+
rename_fields: dict[str, str] | None = None,
|
|
155
134
|
rename_fields_keep_missing: bool = False,
|
|
156
|
-
static_fields:
|
|
157
|
-
reserved_attrs:
|
|
158
|
-
timestamp:
|
|
159
|
-
defaults:
|
|
135
|
+
static_fields: dict[str, Any] | None = None,
|
|
136
|
+
reserved_attrs: Sequence[str] | None = None,
|
|
137
|
+
timestamp: bool | str = False,
|
|
138
|
+
defaults: dict[str, Any] | None = None,
|
|
160
139
|
exc_info_as_array: bool = False,
|
|
161
140
|
stack_info_as_array: bool = False,
|
|
162
141
|
) -> None:
|
|
163
142
|
"""
|
|
164
143
|
Args:
|
|
165
|
-
fmt:
|
|
144
|
+
fmt: String format or `Sequence` of field names of fields to log.
|
|
166
145
|
datefmt: format to use when formatting `asctime` field
|
|
167
|
-
style: how to extract log fields from `fmt`
|
|
146
|
+
style: how to extract log fields from `fmt`. Ignored if `fmt` is a `Sequence[str]`.
|
|
168
147
|
validate: validate `fmt` against style, if implementing a custom `style` you
|
|
169
|
-
must set this to `False`.
|
|
148
|
+
must set this to `False`. Ignored if `fmt` is a `Sequence[str]`.
|
|
170
149
|
defaults: a dictionary containing default fields that are added before all other fields and
|
|
171
150
|
may be overridden. The supplied fields are still subject to `rename_fields`.
|
|
172
151
|
prefix: an optional string prefix added at the beginning of
|
|
@@ -192,23 +171,40 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
192
171
|
- Renaming fields now preserves the order that fields were added in and avoids adding
|
|
193
172
|
missing fields. The original behaviour, missing fields have a value of `None`, is still
|
|
194
173
|
available by setting `rename_fields_keep_missing` to `True`.
|
|
174
|
+
|
|
175
|
+
*Added in 4.0*:
|
|
176
|
+
|
|
177
|
+
- `fmt` now supports comma seperated lists (`style=","`). Note that this style is specific
|
|
178
|
+
to `python-json-logger` and thus care should be taken to not to pass this format to other
|
|
179
|
+
logging Formatter implementations.
|
|
180
|
+
- `fmt` now supports sequences of strings (e.g. lists and tuples) of field names.
|
|
195
181
|
"""
|
|
196
182
|
## logging.Formatter compatibility
|
|
197
183
|
## ---------------------------------------------------------------------
|
|
198
|
-
# Note: validate added in 3.8, defaults added in 3.10
|
|
199
|
-
if
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
184
|
+
# Note: validate added in python 3.8, defaults added in 3.10
|
|
185
|
+
if fmt is None or isinstance(fmt, str):
|
|
186
|
+
if style in logging._STYLES:
|
|
187
|
+
_style = logging._STYLES[style][0](fmt) # type: ignore[operator]
|
|
188
|
+
if validate:
|
|
189
|
+
_style.validate()
|
|
190
|
+
self._style = _style
|
|
191
|
+
self._fmt = _style._fmt
|
|
192
|
+
|
|
193
|
+
elif style == "," or not validate:
|
|
194
|
+
self._style = style
|
|
195
|
+
self._fmt = fmt
|
|
196
|
+
# TODO: Validate comma format
|
|
197
|
+
|
|
198
|
+
else:
|
|
199
|
+
raise ValueError("Style must be one of: '%{$,'")
|
|
200
|
+
|
|
201
|
+
self._required_fields = self.parse()
|
|
202
|
+
|
|
203
|
+
# Note: we do this check second as string is still a Sequence[str]
|
|
204
|
+
elif isinstance(fmt, Sequence):
|
|
205
|
+
self._style = "__sequence__"
|
|
206
|
+
self._fmt = str(fmt)
|
|
207
|
+
self._required_fields = list(fmt)
|
|
212
208
|
|
|
213
209
|
self.datefmt = datefmt
|
|
214
210
|
|
|
@@ -230,7 +226,6 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
230
226
|
self.reserved_attrs = set(reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS)
|
|
231
227
|
self.timestamp = timestamp
|
|
232
228
|
|
|
233
|
-
self._required_fields = self.parse()
|
|
234
229
|
self._skip_fields = set(self._required_fields)
|
|
235
230
|
self._skip_fields.update(self.reserved_attrs)
|
|
236
231
|
self.defaults = defaults if defaults is not None else {}
|
|
@@ -244,7 +239,7 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
244
239
|
Args:
|
|
245
240
|
record: the record to format
|
|
246
241
|
"""
|
|
247
|
-
message_dict:
|
|
242
|
+
message_dict: dict[str, Any] = {}
|
|
248
243
|
# TODO: logging.LogRecord.msg and logging.LogRecord.message in typeshed
|
|
249
244
|
# are always type of str. We shouldn't need to override that.
|
|
250
245
|
if isinstance(record.msg, dict):
|
|
@@ -269,15 +264,15 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
269
264
|
if record.stack_info and not message_dict.get("stack_info"):
|
|
270
265
|
message_dict["stack_info"] = self.formatStack(record.stack_info)
|
|
271
266
|
|
|
272
|
-
|
|
273
|
-
self.add_fields(
|
|
274
|
-
|
|
267
|
+
log_data: LogData = {}
|
|
268
|
+
self.add_fields(log_data, record, message_dict)
|
|
269
|
+
log_data = self.process_log_record(log_data)
|
|
275
270
|
|
|
276
|
-
return self.serialize_log_record(
|
|
271
|
+
return self.serialize_log_record(log_data)
|
|
277
272
|
|
|
278
273
|
## JSON Formatter Specific Methods
|
|
279
274
|
## -------------------------------------------------------------------------
|
|
280
|
-
def parse(self) ->
|
|
275
|
+
def parse(self) -> list[str]:
|
|
281
276
|
"""Parses format string looking for substitutions
|
|
282
277
|
|
|
283
278
|
This method is responsible for returning a list of fields (as strings)
|
|
@@ -288,6 +283,18 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
288
283
|
Returns:
|
|
289
284
|
list of fields to be extracted and serialized
|
|
290
285
|
"""
|
|
286
|
+
if self._fmt is None:
|
|
287
|
+
return []
|
|
288
|
+
|
|
289
|
+
if isinstance(self._style, str):
|
|
290
|
+
if self._style == "__sequence__":
|
|
291
|
+
raise RuntimeError("Must not call parse when fmt is a sequence of strings")
|
|
292
|
+
|
|
293
|
+
if self._style == ",":
|
|
294
|
+
return [field.strip() for field in self._fmt.split(",") if field.strip()]
|
|
295
|
+
|
|
296
|
+
raise ValueError(f"Style {self._style!r} is not supported")
|
|
297
|
+
|
|
291
298
|
if isinstance(self._style, logging.StringTemplateStyle):
|
|
292
299
|
formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX
|
|
293
300
|
|
|
@@ -302,62 +309,63 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
302
309
|
else:
|
|
303
310
|
raise ValueError(f"Style {self._style!r} is not supported")
|
|
304
311
|
|
|
305
|
-
|
|
306
|
-
return formatter_style_pattern.findall(self._fmt)
|
|
307
|
-
|
|
308
|
-
return []
|
|
312
|
+
return formatter_style_pattern.findall(self._fmt)
|
|
309
313
|
|
|
310
|
-
def serialize_log_record(self,
|
|
311
|
-
"""Returns the final representation of the
|
|
314
|
+
def serialize_log_record(self, log_data: LogData) -> str:
|
|
315
|
+
"""Returns the final representation of the data to be logged
|
|
312
316
|
|
|
313
317
|
Args:
|
|
314
|
-
|
|
318
|
+
log_data: the data
|
|
319
|
+
|
|
320
|
+
*Changed in 4.0*: `log_record` renamed to `log_data`
|
|
315
321
|
"""
|
|
316
|
-
return self.prefix + self.jsonify_log_record(
|
|
322
|
+
return self.prefix + self.jsonify_log_record(log_data)
|
|
317
323
|
|
|
318
324
|
def add_fields(
|
|
319
325
|
self,
|
|
320
|
-
|
|
326
|
+
log_data: dict[str, Any],
|
|
321
327
|
record: logging.LogRecord,
|
|
322
|
-
message_dict:
|
|
328
|
+
message_dict: dict[str, Any],
|
|
323
329
|
) -> None:
|
|
324
330
|
"""Extract fields from a LogRecord for logging
|
|
325
331
|
|
|
326
332
|
This method can be overridden to implement custom logic for adding fields.
|
|
327
333
|
|
|
328
334
|
Args:
|
|
329
|
-
|
|
335
|
+
log_data: data that will be logged
|
|
330
336
|
record: the record to extract data from
|
|
331
337
|
message_dict: dictionary that was logged instead of a message. e.g
|
|
332
338
|
`logger.info({"is_this_message_dict": True})`
|
|
339
|
+
|
|
340
|
+
*Changed in 4.0*: `log_record` renamed to `log_data`
|
|
333
341
|
"""
|
|
334
342
|
for field in self.defaults:
|
|
335
|
-
|
|
343
|
+
log_data[self._get_rename(field)] = self.defaults[field]
|
|
336
344
|
|
|
337
345
|
for field in self._required_fields:
|
|
338
|
-
|
|
346
|
+
log_data[self._get_rename(field)] = record.__dict__.get(field)
|
|
339
347
|
|
|
340
348
|
for data_dict in [self.static_fields, message_dict]:
|
|
341
349
|
for key, value in data_dict.items():
|
|
342
|
-
|
|
350
|
+
log_data[self._get_rename(key)] = value
|
|
343
351
|
|
|
344
352
|
merge_record_extra(
|
|
345
353
|
record,
|
|
346
|
-
|
|
354
|
+
log_data,
|
|
347
355
|
reserved=self._skip_fields,
|
|
348
356
|
rename_fields=self.rename_fields,
|
|
349
357
|
)
|
|
350
358
|
|
|
351
359
|
if self.timestamp:
|
|
352
360
|
key = self.timestamp if isinstance(self.timestamp, str) else "timestamp"
|
|
353
|
-
|
|
361
|
+
log_data[self._get_rename(key)] = datetime.fromtimestamp(
|
|
354
362
|
record.created, tz=timezone.utc
|
|
355
363
|
)
|
|
356
364
|
|
|
357
365
|
if self.rename_fields_keep_missing:
|
|
358
366
|
for field in self.rename_fields.values():
|
|
359
|
-
if field not in
|
|
360
|
-
|
|
367
|
+
if field not in log_data:
|
|
368
|
+
log_data[field] = None
|
|
361
369
|
return
|
|
362
370
|
|
|
363
371
|
def _get_rename(self, key: str) -> str:
|
|
@@ -365,28 +373,32 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
365
373
|
|
|
366
374
|
# Child Methods
|
|
367
375
|
# ..........................................................................
|
|
368
|
-
def jsonify_log_record(self,
|
|
369
|
-
"""Convert
|
|
376
|
+
def jsonify_log_record(self, log_data: LogData) -> str:
|
|
377
|
+
"""Convert the log data into a JSON string.
|
|
370
378
|
|
|
371
379
|
Child classes MUST override this method.
|
|
372
380
|
|
|
373
381
|
Args:
|
|
374
|
-
|
|
382
|
+
log_data: the data to serialize
|
|
383
|
+
|
|
384
|
+
*Changed in 4.0*: `log_record` renamed to `log_data`
|
|
375
385
|
"""
|
|
376
386
|
raise NotImplementedError()
|
|
377
387
|
|
|
378
|
-
def process_log_record(self,
|
|
379
|
-
"""Custom processing of the
|
|
388
|
+
def process_log_record(self, log_data: LogData) -> LogData:
|
|
389
|
+
"""Custom processing of the data to be logged.
|
|
380
390
|
|
|
381
391
|
Child classes can override this method to alter the log record before it
|
|
382
392
|
is serialized.
|
|
383
393
|
|
|
384
394
|
Args:
|
|
385
|
-
|
|
395
|
+
log_data: incoming data
|
|
396
|
+
|
|
397
|
+
*Changed in 4.0*: `log_record` renamed to `log_data`
|
|
386
398
|
"""
|
|
387
|
-
return
|
|
399
|
+
return log_data
|
|
388
400
|
|
|
389
|
-
def formatException(self, ei) ->
|
|
401
|
+
def formatException(self, ei) -> str | list[str]: # type: ignore[override]
|
|
390
402
|
"""Format and return the specified exception information.
|
|
391
403
|
|
|
392
404
|
If exc_info_as_array is set to True, This method returns an array of strings.
|
|
@@ -394,7 +406,7 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
394
406
|
exception_info_str = super().formatException(ei)
|
|
395
407
|
return exception_info_str.splitlines() if self.exc_info_as_array else exception_info_str
|
|
396
408
|
|
|
397
|
-
def formatStack(self, stack_info) ->
|
|
409
|
+
def formatStack(self, stack_info) -> str | list[str]: # type: ignore[override]
|
|
398
410
|
"""Format and return the specified stack information.
|
|
399
411
|
|
|
400
412
|
If stack_info_as_array is set to True, This method returns an array of strings.
|
|
@@ -16,17 +16,11 @@ import base64
|
|
|
16
16
|
import dataclasses
|
|
17
17
|
import datetime
|
|
18
18
|
import enum
|
|
19
|
-
import sys
|
|
20
19
|
from types import TracebackType
|
|
21
|
-
from typing import Any
|
|
20
|
+
from typing import Any, TypeGuard
|
|
22
21
|
import traceback
|
|
23
22
|
import uuid
|
|
24
23
|
|
|
25
|
-
if sys.version_info >= (3, 10):
|
|
26
|
-
from typing import TypeGuard
|
|
27
|
-
else:
|
|
28
|
-
from typing_extensions import TypeGuard
|
|
29
|
-
|
|
30
24
|
## Installed
|
|
31
25
|
|
|
32
26
|
## Application
|
|
@@ -12,7 +12,8 @@ from __future__ import annotations
|
|
|
12
12
|
## Standard Library
|
|
13
13
|
import datetime
|
|
14
14
|
import json
|
|
15
|
-
from typing import Any
|
|
15
|
+
from typing import Any
|
|
16
|
+
from collections.abc import Callable
|
|
16
17
|
import warnings
|
|
17
18
|
|
|
18
19
|
## Application
|
|
@@ -67,10 +68,10 @@ class JsonFormatter(core.BaseJsonFormatter):
|
|
|
67
68
|
def __init__(
|
|
68
69
|
self,
|
|
69
70
|
*args,
|
|
70
|
-
json_default:
|
|
71
|
-
json_encoder:
|
|
72
|
-
json_serializer:
|
|
73
|
-
json_indent:
|
|
71
|
+
json_default: Callable | None = None,
|
|
72
|
+
json_encoder: Callable | None = None,
|
|
73
|
+
json_serializer: Callable = json.dumps,
|
|
74
|
+
json_indent: int | str | None = None,
|
|
74
75
|
json_ensure_ascii: bool = True,
|
|
75
76
|
**kwargs,
|
|
76
77
|
) -> None:
|
|
@@ -87,19 +88,19 @@ class JsonFormatter(core.BaseJsonFormatter):
|
|
|
87
88
|
"""
|
|
88
89
|
super().__init__(*args, **kwargs)
|
|
89
90
|
|
|
90
|
-
self.json_default =
|
|
91
|
-
self.json_encoder =
|
|
92
|
-
self.json_serializer =
|
|
91
|
+
self.json_default = json_default
|
|
92
|
+
self.json_encoder = json_encoder
|
|
93
|
+
self.json_serializer = json_serializer
|
|
93
94
|
self.json_indent = json_indent
|
|
94
95
|
self.json_ensure_ascii = json_ensure_ascii
|
|
95
96
|
if not self.json_encoder and not self.json_default:
|
|
96
97
|
self.json_encoder = JsonEncoder
|
|
97
98
|
return
|
|
98
99
|
|
|
99
|
-
def jsonify_log_record(self,
|
|
100
|
-
"""Returns a json string of the log
|
|
100
|
+
def jsonify_log_record(self, log_data: core.LogData) -> str:
|
|
101
|
+
"""Returns a json string of the log data."""
|
|
101
102
|
return self.json_serializer(
|
|
102
|
-
|
|
103
|
+
log_data,
|
|
103
104
|
default=self.json_default,
|
|
104
105
|
cls=self.json_encoder,
|
|
105
106
|
indent=self.json_indent,
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
## Standard Library
|
|
9
9
|
from typing import Any
|
|
10
|
+
from collections.abc import Callable
|
|
10
11
|
|
|
11
12
|
## Installed
|
|
12
13
|
|
|
@@ -43,7 +44,7 @@ class MsgspecFormatter(core.BaseJsonFormatter):
|
|
|
43
44
|
def __init__(
|
|
44
45
|
self,
|
|
45
46
|
*args,
|
|
46
|
-
json_default:
|
|
47
|
+
json_default: Callable | None = msgspec_default,
|
|
47
48
|
**kwargs,
|
|
48
49
|
) -> None:
|
|
49
50
|
"""
|
|
@@ -54,10 +55,10 @@ class MsgspecFormatter(core.BaseJsonFormatter):
|
|
|
54
55
|
"""
|
|
55
56
|
super().__init__(*args, **kwargs)
|
|
56
57
|
|
|
57
|
-
self.json_default =
|
|
58
|
+
self.json_default = json_default
|
|
58
59
|
self._encoder = msgspec.json.Encoder(enc_hook=self.json_default)
|
|
59
60
|
return
|
|
60
61
|
|
|
61
|
-
def jsonify_log_record(self,
|
|
62
|
-
"""Returns a json string of the log
|
|
63
|
-
return self._encoder.encode(
|
|
62
|
+
def jsonify_log_record(self, log_data: core.LogData) -> str:
|
|
63
|
+
"""Returns a json string of the log data."""
|
|
64
|
+
return self._encoder.encode(log_data).decode("utf8")
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
## Standard Library
|
|
9
9
|
from typing import Any
|
|
10
|
+
from collections.abc import Callable
|
|
10
11
|
|
|
11
12
|
## Installed
|
|
12
13
|
|
|
@@ -15,7 +16,7 @@ from . import core
|
|
|
15
16
|
from . import defaults as d
|
|
16
17
|
from .utils import package_is_available
|
|
17
18
|
|
|
18
|
-
# We import
|
|
19
|
+
# We import orjson after checking it is available
|
|
19
20
|
package_is_available("orjson", throw_error=True)
|
|
20
21
|
import orjson # pylint: disable=wrong-import-position,wrong-import-order
|
|
21
22
|
|
|
@@ -45,7 +46,7 @@ class OrjsonFormatter(core.BaseJsonFormatter):
|
|
|
45
46
|
def __init__(
|
|
46
47
|
self,
|
|
47
48
|
*args,
|
|
48
|
-
json_default:
|
|
49
|
+
json_default: Callable | None = orjson_default,
|
|
49
50
|
json_indent: bool = False,
|
|
50
51
|
**kwargs,
|
|
51
52
|
) -> None:
|
|
@@ -58,14 +59,14 @@ class OrjsonFormatter(core.BaseJsonFormatter):
|
|
|
58
59
|
"""
|
|
59
60
|
super().__init__(*args, **kwargs)
|
|
60
61
|
|
|
61
|
-
self.json_default =
|
|
62
|
+
self.json_default = json_default
|
|
62
63
|
self.json_indent = json_indent
|
|
63
64
|
return
|
|
64
65
|
|
|
65
|
-
def jsonify_log_record(self,
|
|
66
|
-
"""Returns a json string of the log
|
|
66
|
+
def jsonify_log_record(self, log_data: core.LogData) -> str:
|
|
67
|
+
"""Returns a json string of the log data."""
|
|
67
68
|
opt = orjson.OPT_NON_STR_KEYS
|
|
68
69
|
if self.json_indent:
|
|
69
70
|
opt |= orjson.OPT_INDENT_2
|
|
70
71
|
|
|
71
|
-
return orjson.dumps(
|
|
72
|
+
return orjson.dumps(log_data, default=self.json_default, option=opt).decode("utf8")
|
|
@@ -9,7 +9,8 @@ import io
|
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
11
|
import logging.config
|
|
12
|
-
from typing import Any
|
|
12
|
+
from typing import Any
|
|
13
|
+
from collections.abc import Generator
|
|
13
14
|
|
|
14
15
|
## Installed
|
|
15
16
|
import pytest
|
|
@@ -19,12 +20,24 @@ import pytest
|
|
|
19
20
|
_LOGGER_COUNT = 0
|
|
20
21
|
EXT_VAL = 999
|
|
21
22
|
|
|
23
|
+
|
|
24
|
+
class Dummy:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def my_json_default(obj: Any) -> Any:
|
|
29
|
+
if isinstance(obj, Dummy):
|
|
30
|
+
return "DUMMY"
|
|
31
|
+
return obj
|
|
32
|
+
|
|
33
|
+
|
|
22
34
|
LOGGING_CONFIG = {
|
|
23
35
|
"version": 1,
|
|
24
36
|
"disable_existing_loggers": False,
|
|
25
37
|
"formatters": {
|
|
26
38
|
"default": {
|
|
27
39
|
"()": "pythonjsonlogger.json.JsonFormatter",
|
|
40
|
+
"json_default": "ext://tests.test_dictconfig.my_json_default",
|
|
28
41
|
"static_fields": {"ext-val": "ext://tests.test_dictconfig.EXT_VAL"},
|
|
29
42
|
}
|
|
30
43
|
},
|
|
@@ -52,7 +65,7 @@ class LoggingEnvironment:
|
|
|
52
65
|
|
|
53
66
|
|
|
54
67
|
@pytest.fixture
|
|
55
|
-
def env() -> Generator[LoggingEnvironment
|
|
68
|
+
def env() -> Generator[LoggingEnvironment]:
|
|
56
69
|
global _LOGGER_COUNT # pylint: disable=global-statement
|
|
57
70
|
_LOGGER_COUNT += 1
|
|
58
71
|
logging.config.dictConfig(LOGGING_CONFIG)
|
|
@@ -73,8 +86,12 @@ def env() -> Generator[LoggingEnvironment, None, None]:
|
|
|
73
86
|
### TESTS
|
|
74
87
|
### ============================================================================
|
|
75
88
|
def test_external_reference_support(env: LoggingEnvironment):
|
|
76
|
-
|
|
89
|
+
|
|
90
|
+
assert logging.root.handlers[0].formatter.json_default is my_json_default # type: ignore[union-attr]
|
|
91
|
+
|
|
92
|
+
env.logger.info("hello", extra={"dummy": Dummy()})
|
|
77
93
|
log_json = env.load_json()
|
|
78
94
|
|
|
79
95
|
assert log_json["ext-val"] == EXT_VAL
|
|
96
|
+
assert log_json["dummy"] == "DUMMY"
|
|
80
97
|
return
|
|
@@ -13,13 +13,10 @@ import logging
|
|
|
13
13
|
import sys
|
|
14
14
|
import traceback
|
|
15
15
|
from types import TracebackType
|
|
16
|
-
from typing import Any
|
|
16
|
+
from typing import Any
|
|
17
|
+
from collections.abc import Generator
|
|
17
18
|
import uuid
|
|
18
|
-
|
|
19
|
-
if sys.version_info >= (3, 9):
|
|
20
|
-
import zoneinfo
|
|
21
|
-
else:
|
|
22
|
-
from backports import zoneinfo
|
|
19
|
+
import zoneinfo
|
|
23
20
|
|
|
24
21
|
## Installed
|
|
25
22
|
import freezegun
|
|
@@ -63,7 +60,7 @@ class LoggingEnvironment:
|
|
|
63
60
|
|
|
64
61
|
|
|
65
62
|
@pytest.fixture
|
|
66
|
-
def env() -> Generator[LoggingEnvironment
|
|
63
|
+
def env() -> Generator[LoggingEnvironment]:
|
|
67
64
|
global _LOGGER_COUNT # pylint: disable=global-statement
|
|
68
65
|
_LOGGER_COUNT += 1
|
|
69
66
|
logger = logging.getLogger(f"pythonjsonlogger.tests.{_LOGGER_COUNT}")
|
|
@@ -158,12 +155,48 @@ def test_default_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]
|
|
|
158
155
|
|
|
159
156
|
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
|
|
160
157
|
def test_percentage_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
158
|
+
# Note: We use different %s styles in the format to check the regex correctly collects them
|
|
159
|
+
env.set_formatter(class_("[%(levelname)8s] %(message)s %(filename)s:%(lineno)d %(asctime)"))
|
|
160
|
+
|
|
161
|
+
msg = "testing logging format"
|
|
162
|
+
env.logger.info(msg)
|
|
163
|
+
log_json = env.load_json()
|
|
164
|
+
|
|
165
|
+
assert log_json["message"] == msg
|
|
166
|
+
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
|
|
171
|
+
def test_comma_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
172
|
+
# Note: we have double comma `,,` to test handling "empty" names
|
|
173
|
+
env.set_formatter(class_("levelname,,message,filename,lineno,asctime,", style=","))
|
|
174
|
+
|
|
175
|
+
msg = "testing logging format"
|
|
176
|
+
env.logger.info(msg)
|
|
177
|
+
log_json = env.load_json()
|
|
178
|
+
|
|
179
|
+
assert log_json["message"] == msg
|
|
180
|
+
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
|
|
185
|
+
def test_sequence_list_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
186
|
+
env.set_formatter(class_(["levelname", "message", "filename", "lineno", "asctime"]))
|
|
187
|
+
|
|
188
|
+
msg = "testing logging format"
|
|
189
|
+
env.logger.info(msg)
|
|
190
|
+
log_json = env.load_json()
|
|
191
|
+
|
|
192
|
+
assert log_json["message"] == msg
|
|
193
|
+
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
|
|
198
|
+
def test_sequence_tuple_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
199
|
+
env.set_formatter(class_(("levelname", "message", "filename", "lineno", "asctime")))
|
|
167
200
|
|
|
168
201
|
msg = "testing logging format"
|
|
169
202
|
env.logger.info(msg)
|
|
@@ -380,9 +413,9 @@ def test_log_extra(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
|
380
413
|
def test_custom_logic_adds_field(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
381
414
|
class CustomJsonFormatter(class_): # type: ignore[valid-type,misc]
|
|
382
415
|
|
|
383
|
-
def process_log_record(self,
|
|
384
|
-
|
|
385
|
-
return super().process_log_record(
|
|
416
|
+
def process_log_record(self, log_data):
|
|
417
|
+
log_data["custom"] = "value"
|
|
418
|
+
return super().process_log_record(log_data)
|
|
386
419
|
|
|
387
420
|
env.set_formatter(CustomJsonFormatter())
|
|
388
421
|
env.logger.info("message")
|
|
@@ -522,6 +555,7 @@ def test_default_encoder_with_timestamp(env: LoggingEnvironment, class_: type[Ba
|
|
|
522
555
|
(False, bool, False),
|
|
523
556
|
(None, type(None), None),
|
|
524
557
|
(b"some-bytes", str, "c29tZS1ieXRlcw=="),
|
|
558
|
+
(b"fancy-bytes-\xf0\xf1", str, "ZmFuY3ktYnl0ZXMt8PE="),
|
|
525
559
|
(datetime.time(16, 45, 30, 100), str, "16:45:30.000100"),
|
|
526
560
|
(datetime.date(2024, 5, 5), str, "2024-05-05"),
|
|
527
561
|
(datetime.datetime(2024, 5, 5, 16, 45, 30, 100), str, "2024-05-05T16:45:30.000100"),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/exception.py
RENAMED
|
File without changes
|
{python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/jsonlogger.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|