python-json-logger 3.3.0__tar.gz → 4.0.0rc1__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-3.3.0/src/python_json_logger.egg-info → python_json_logger-4.0.0rc1}/PKG-INFO +3 -2
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/pyproject.toml +1 -1
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1/src/python_json_logger.egg-info}/PKG-INFO +3 -2
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/python_json_logger.egg-info/SOURCES.txt +1 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/core.py +97 -72
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/json.py +9 -9
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/msgspec.py +6 -6
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/orjson.py +6 -6
- python_json_logger-4.0.0rc1/tests/test_dictconfig.py +96 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/tests/test_formatters.py +45 -9
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/LICENSE +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/MANIFEST.in +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/NOTICE +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/README.md +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/setup.cfg +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/python_json_logger.egg-info/dependency_links.txt +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/python_json_logger.egg-info/requires.txt +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/python_json_logger.egg-info/top_level.txt +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/__init__.py +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/defaults.py +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/exception.py +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/jsonlogger.py +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/py.typed +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/utils.py +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/tests/__init__.py +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/tests/test_deprecation.py +0 -0
- {python_json_logger-3.3.0 → python_json_logger-4.0.0rc1}/tests/test_missing.py +0 -0
{python_json_logger-3.3.0/src/python_json_logger.egg-info → python_json_logger-4.0.0rc1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-json-logger
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0rc1
|
|
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>
|
|
@@ -45,6 +45,7 @@ Requires-Dist: mkdocstrings[python]; extra == "dev"
|
|
|
45
45
|
Requires-Dist: mkdocs-gen-files; extra == "dev"
|
|
46
46
|
Requires-Dist: mkdocs-literate-nav; extra == "dev"
|
|
47
47
|
Requires-Dist: mike; extra == "dev"
|
|
48
|
+
Dynamic: license-file
|
|
48
49
|
|
|
49
50
|
[](https://pypi.python.org/pypi/python-json-logger/)
|
|
50
51
|
[](https://pypi.python.org/pypi/python-json-logger/)
|
{python_json_logger-3.3.0 → python_json_logger-4.0.0rc1/src/python_json_logger.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-json-logger
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0rc1
|
|
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>
|
|
@@ -45,6 +45,7 @@ Requires-Dist: mkdocstrings[python]; extra == "dev"
|
|
|
45
45
|
Requires-Dist: mkdocs-gen-files; extra == "dev"
|
|
46
46
|
Requires-Dist: mkdocs-literate-nav; extra == "dev"
|
|
47
47
|
Requires-Dist: mike; extra == "dev"
|
|
48
|
+
Dynamic: license-file
|
|
48
49
|
|
|
49
50
|
[](https://pypi.python.org/pypi/python-json-logger/)
|
|
50
51
|
[](https://pypi.python.org/pypi/python-json-logger/)
|
|
@@ -7,11 +7,10 @@ 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 Optional, Union,
|
|
13
|
+
from typing import Optional, Union, List, Dict, Container, Any, Sequence
|
|
15
14
|
|
|
16
15
|
if sys.version_info >= (3, 10):
|
|
17
16
|
from typing import TypeAlias
|
|
@@ -72,31 +71,15 @@ STYLE_PERCENT_REGEX = re.compile(r"%\((.+?)\)", re.IGNORECASE) # % style
|
|
|
72
71
|
|
|
73
72
|
## Type Aliases
|
|
74
73
|
## -----------------------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
"""Type alias
|
|
74
|
+
LogData: TypeAlias = Dict[str, Any]
|
|
75
|
+
"""Type alias
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
"""
|
|
77
|
+
*Changed in 4.0*: renamed from `LogRecord` to `LogData`
|
|
78
|
+
"""
|
|
80
79
|
|
|
81
80
|
|
|
82
81
|
### FUNCTIONS
|
|
83
82
|
### ============================================================================
|
|
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
83
|
def merge_record_extra(
|
|
101
84
|
record: logging.LogRecord,
|
|
102
85
|
target: Dict,
|
|
@@ -135,7 +118,7 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
135
118
|
|
|
136
119
|
*Changed in 3.2*: `defaults` argument is no longer ignored.
|
|
137
120
|
|
|
138
|
-
*Added in
|
|
121
|
+
*Added in 3.3*: `exc_info_as_array` and `stack_info_as_array` options are added.
|
|
139
122
|
"""
|
|
140
123
|
|
|
141
124
|
_style: Union[logging.PercentStyle, str] # type: ignore[assignment]
|
|
@@ -145,7 +128,7 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
145
128
|
# pylint: disable=too-many-arguments,super-init-not-called
|
|
146
129
|
def __init__(
|
|
147
130
|
self,
|
|
148
|
-
fmt: Optional[str] = None,
|
|
131
|
+
fmt: Optional[Union[str, Sequence[str]]] = None,
|
|
149
132
|
datefmt: Optional[str] = None,
|
|
150
133
|
style: str = "%",
|
|
151
134
|
validate: bool = True,
|
|
@@ -162,11 +145,11 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
162
145
|
) -> None:
|
|
163
146
|
"""
|
|
164
147
|
Args:
|
|
165
|
-
fmt:
|
|
148
|
+
fmt: String format or `Sequence` of field names of fields to log.
|
|
166
149
|
datefmt: format to use when formatting `asctime` field
|
|
167
|
-
style: how to extract log fields from `fmt`
|
|
150
|
+
style: how to extract log fields from `fmt`. Ignored if `fmt` is a `Sequence[str]`.
|
|
168
151
|
validate: validate `fmt` against style, if implementing a custom `style` you
|
|
169
|
-
must set this to `False`.
|
|
152
|
+
must set this to `False`. Ignored if `fmt` is a `Sequence[str]`.
|
|
170
153
|
defaults: a dictionary containing default fields that are added before all other fields and
|
|
171
154
|
may be overridden. The supplied fields are still subject to `rename_fields`.
|
|
172
155
|
prefix: an optional string prefix added at the beginning of
|
|
@@ -192,36 +175,61 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
192
175
|
- Renaming fields now preserves the order that fields were added in and avoids adding
|
|
193
176
|
missing fields. The original behaviour, missing fields have a value of `None`, is still
|
|
194
177
|
available by setting `rename_fields_keep_missing` to `True`.
|
|
178
|
+
|
|
179
|
+
*Added in 4.0*:
|
|
180
|
+
|
|
181
|
+
- `fmt` now supports comma seperated lists (`style=","`). Note that this style is specific
|
|
182
|
+
to `python-json-logger` and thus care should be taken to not to pass this format to other
|
|
183
|
+
logging Formatter implementations.
|
|
184
|
+
- `fmt` now supports sequences of strings (e.g. lists and tuples) of field names.
|
|
195
185
|
"""
|
|
196
186
|
## logging.Formatter compatibility
|
|
197
187
|
## ---------------------------------------------------------------------
|
|
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
|
-
|
|
188
|
+
# Note: validate added in python 3.8, defaults added in 3.10
|
|
189
|
+
if fmt is None or isinstance(fmt, str):
|
|
190
|
+
if style in logging._STYLES:
|
|
191
|
+
_style = logging._STYLES[style][0](fmt) # type: ignore[operator]
|
|
192
|
+
if validate:
|
|
193
|
+
_style.validate()
|
|
194
|
+
self._style = _style
|
|
195
|
+
self._fmt = _style._fmt
|
|
196
|
+
|
|
197
|
+
elif style == "," or not validate:
|
|
198
|
+
self._style = style
|
|
199
|
+
self._fmt = fmt
|
|
200
|
+
# TODO: Validate comma format
|
|
201
|
+
|
|
202
|
+
else:
|
|
203
|
+
raise ValueError("Style must be one of: '%{$,'")
|
|
204
|
+
|
|
205
|
+
self._required_fields = self.parse()
|
|
206
|
+
|
|
207
|
+
# Note: we do this check second as string is still a Sequence[str]
|
|
208
|
+
elif isinstance(fmt, Sequence):
|
|
209
|
+
self._style = "__sequence__"
|
|
210
|
+
self._fmt = str(fmt)
|
|
211
|
+
self._required_fields = list(fmt)
|
|
212
212
|
|
|
213
213
|
self.datefmt = datefmt
|
|
214
214
|
|
|
215
215
|
## JSON Logging specific
|
|
216
216
|
## ---------------------------------------------------------------------
|
|
217
217
|
self.prefix = prefix
|
|
218
|
-
|
|
218
|
+
|
|
219
|
+
# We recreate the dict in rename_fields and static_fields to support internal/external
|
|
220
|
+
# references which require getting the item to do the conversion.
|
|
221
|
+
# For more details see: https://github.com/nhairs/python-json-logger/pull/45
|
|
222
|
+
self.rename_fields = (
|
|
223
|
+
{key: rename_fields[key] for key in rename_fields} if rename_fields is not None else {}
|
|
224
|
+
)
|
|
225
|
+
self.static_fields = (
|
|
226
|
+
{key: static_fields[key] for key in static_fields} if static_fields is not None else {}
|
|
227
|
+
)
|
|
228
|
+
|
|
219
229
|
self.rename_fields_keep_missing = rename_fields_keep_missing
|
|
220
|
-
self.static_fields = static_fields if static_fields is not None else {}
|
|
221
230
|
self.reserved_attrs = set(reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS)
|
|
222
231
|
self.timestamp = timestamp
|
|
223
232
|
|
|
224
|
-
self._required_fields = self.parse()
|
|
225
233
|
self._skip_fields = set(self._required_fields)
|
|
226
234
|
self._skip_fields.update(self.reserved_attrs)
|
|
227
235
|
self.defaults = defaults if defaults is not None else {}
|
|
@@ -260,11 +268,11 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
260
268
|
if record.stack_info and not message_dict.get("stack_info"):
|
|
261
269
|
message_dict["stack_info"] = self.formatStack(record.stack_info)
|
|
262
270
|
|
|
263
|
-
|
|
264
|
-
self.add_fields(
|
|
265
|
-
|
|
271
|
+
log_data: LogData = {}
|
|
272
|
+
self.add_fields(log_data, record, message_dict)
|
|
273
|
+
log_data = self.process_log_record(log_data)
|
|
266
274
|
|
|
267
|
-
return self.serialize_log_record(
|
|
275
|
+
return self.serialize_log_record(log_data)
|
|
268
276
|
|
|
269
277
|
## JSON Formatter Specific Methods
|
|
270
278
|
## -------------------------------------------------------------------------
|
|
@@ -279,6 +287,18 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
279
287
|
Returns:
|
|
280
288
|
list of fields to be extracted and serialized
|
|
281
289
|
"""
|
|
290
|
+
if self._fmt is None:
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
if isinstance(self._style, str):
|
|
294
|
+
if self._style == "__sequence__":
|
|
295
|
+
raise RuntimeError("Must not call parse when fmt is a sequence of strings")
|
|
296
|
+
|
|
297
|
+
if self._style == ",":
|
|
298
|
+
return [field.strip() for field in self._fmt.split(",") if field.strip()]
|
|
299
|
+
|
|
300
|
+
raise ValueError(f"Style {self._style!r} is not supported")
|
|
301
|
+
|
|
282
302
|
if isinstance(self._style, logging.StringTemplateStyle):
|
|
283
303
|
formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX
|
|
284
304
|
|
|
@@ -293,22 +313,21 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
293
313
|
else:
|
|
294
314
|
raise ValueError(f"Style {self._style!r} is not supported")
|
|
295
315
|
|
|
296
|
-
|
|
297
|
-
return formatter_style_pattern.findall(self._fmt)
|
|
316
|
+
return formatter_style_pattern.findall(self._fmt)
|
|
298
317
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def serialize_log_record(self, log_record: LogRecord) -> str:
|
|
302
|
-
"""Returns the final representation of the log record.
|
|
318
|
+
def serialize_log_record(self, log_data: LogData) -> str:
|
|
319
|
+
"""Returns the final representation of the data to be logged
|
|
303
320
|
|
|
304
321
|
Args:
|
|
305
|
-
|
|
322
|
+
log_data: the data
|
|
323
|
+
|
|
324
|
+
*Changed in 4.0*: `log_record` renamed to `log_data`
|
|
306
325
|
"""
|
|
307
|
-
return self.prefix + self.jsonify_log_record(
|
|
326
|
+
return self.prefix + self.jsonify_log_record(log_data)
|
|
308
327
|
|
|
309
328
|
def add_fields(
|
|
310
329
|
self,
|
|
311
|
-
|
|
330
|
+
log_data: Dict[str, Any],
|
|
312
331
|
record: logging.LogRecord,
|
|
313
332
|
message_dict: Dict[str, Any],
|
|
314
333
|
) -> None:
|
|
@@ -317,38 +336,40 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
317
336
|
This method can be overridden to implement custom logic for adding fields.
|
|
318
337
|
|
|
319
338
|
Args:
|
|
320
|
-
|
|
339
|
+
log_data: data that will be logged
|
|
321
340
|
record: the record to extract data from
|
|
322
341
|
message_dict: dictionary that was logged instead of a message. e.g
|
|
323
342
|
`logger.info({"is_this_message_dict": True})`
|
|
343
|
+
|
|
344
|
+
*Changed in 4.0*: `log_record` renamed to `log_data`
|
|
324
345
|
"""
|
|
325
346
|
for field in self.defaults:
|
|
326
|
-
|
|
347
|
+
log_data[self._get_rename(field)] = self.defaults[field]
|
|
327
348
|
|
|
328
349
|
for field in self._required_fields:
|
|
329
|
-
|
|
350
|
+
log_data[self._get_rename(field)] = record.__dict__.get(field)
|
|
330
351
|
|
|
331
352
|
for data_dict in [self.static_fields, message_dict]:
|
|
332
353
|
for key, value in data_dict.items():
|
|
333
|
-
|
|
354
|
+
log_data[self._get_rename(key)] = value
|
|
334
355
|
|
|
335
356
|
merge_record_extra(
|
|
336
357
|
record,
|
|
337
|
-
|
|
358
|
+
log_data,
|
|
338
359
|
reserved=self._skip_fields,
|
|
339
360
|
rename_fields=self.rename_fields,
|
|
340
361
|
)
|
|
341
362
|
|
|
342
363
|
if self.timestamp:
|
|
343
364
|
key = self.timestamp if isinstance(self.timestamp, str) else "timestamp"
|
|
344
|
-
|
|
365
|
+
log_data[self._get_rename(key)] = datetime.fromtimestamp(
|
|
345
366
|
record.created, tz=timezone.utc
|
|
346
367
|
)
|
|
347
368
|
|
|
348
369
|
if self.rename_fields_keep_missing:
|
|
349
370
|
for field in self.rename_fields.values():
|
|
350
|
-
if field not in
|
|
351
|
-
|
|
371
|
+
if field not in log_data:
|
|
372
|
+
log_data[field] = None
|
|
352
373
|
return
|
|
353
374
|
|
|
354
375
|
def _get_rename(self, key: str) -> str:
|
|
@@ -356,26 +377,30 @@ class BaseJsonFormatter(logging.Formatter):
|
|
|
356
377
|
|
|
357
378
|
# Child Methods
|
|
358
379
|
# ..........................................................................
|
|
359
|
-
def jsonify_log_record(self,
|
|
360
|
-
"""Convert
|
|
380
|
+
def jsonify_log_record(self, log_data: LogData) -> str:
|
|
381
|
+
"""Convert the log data into a JSON string.
|
|
361
382
|
|
|
362
383
|
Child classes MUST override this method.
|
|
363
384
|
|
|
364
385
|
Args:
|
|
365
|
-
|
|
386
|
+
log_data: the data to serialize
|
|
387
|
+
|
|
388
|
+
*Changed in 4.0*: `log_record` renamed to `log_data`
|
|
366
389
|
"""
|
|
367
390
|
raise NotImplementedError()
|
|
368
391
|
|
|
369
|
-
def process_log_record(self,
|
|
370
|
-
"""Custom processing of the
|
|
392
|
+
def process_log_record(self, log_data: LogData) -> LogData:
|
|
393
|
+
"""Custom processing of the data to be logged.
|
|
371
394
|
|
|
372
395
|
Child classes can override this method to alter the log record before it
|
|
373
396
|
is serialized.
|
|
374
397
|
|
|
375
398
|
Args:
|
|
376
|
-
|
|
399
|
+
log_data: incoming data
|
|
400
|
+
|
|
401
|
+
*Changed in 4.0*: `log_record` renamed to `log_data`
|
|
377
402
|
"""
|
|
378
|
-
return
|
|
403
|
+
return log_data
|
|
379
404
|
|
|
380
405
|
def formatException(self, ei) -> Union[str, list[str]]: # type: ignore
|
|
381
406
|
"""Format and return the specified exception information.
|
|
@@ -67,9 +67,9 @@ class JsonFormatter(core.BaseJsonFormatter):
|
|
|
67
67
|
def __init__(
|
|
68
68
|
self,
|
|
69
69
|
*args,
|
|
70
|
-
json_default:
|
|
71
|
-
json_encoder:
|
|
72
|
-
json_serializer:
|
|
70
|
+
json_default: Optional[Callable] = None,
|
|
71
|
+
json_encoder: Optional[Callable] = None,
|
|
72
|
+
json_serializer: Callable = json.dumps,
|
|
73
73
|
json_indent: Optional[Union[int, str]] = None,
|
|
74
74
|
json_ensure_ascii: bool = True,
|
|
75
75
|
**kwargs,
|
|
@@ -87,19 +87,19 @@ class JsonFormatter(core.BaseJsonFormatter):
|
|
|
87
87
|
"""
|
|
88
88
|
super().__init__(*args, **kwargs)
|
|
89
89
|
|
|
90
|
-
self.json_default =
|
|
91
|
-
self.json_encoder =
|
|
92
|
-
self.json_serializer =
|
|
90
|
+
self.json_default = json_default
|
|
91
|
+
self.json_encoder = json_encoder
|
|
92
|
+
self.json_serializer = json_serializer
|
|
93
93
|
self.json_indent = json_indent
|
|
94
94
|
self.json_ensure_ascii = json_ensure_ascii
|
|
95
95
|
if not self.json_encoder and not self.json_default:
|
|
96
96
|
self.json_encoder = JsonEncoder
|
|
97
97
|
return
|
|
98
98
|
|
|
99
|
-
def jsonify_log_record(self,
|
|
100
|
-
"""Returns a json string of the log
|
|
99
|
+
def jsonify_log_record(self, log_data: core.LogData) -> str:
|
|
100
|
+
"""Returns a json string of the log data."""
|
|
101
101
|
return self.json_serializer(
|
|
102
|
-
|
|
102
|
+
log_data,
|
|
103
103
|
default=self.json_default,
|
|
104
104
|
cls=self.json_encoder,
|
|
105
105
|
indent=self.json_indent,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
## Standard Library
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, Optional, Callable
|
|
10
10
|
|
|
11
11
|
## Installed
|
|
12
12
|
|
|
@@ -43,7 +43,7 @@ class MsgspecFormatter(core.BaseJsonFormatter):
|
|
|
43
43
|
def __init__(
|
|
44
44
|
self,
|
|
45
45
|
*args,
|
|
46
|
-
json_default:
|
|
46
|
+
json_default: Optional[Callable] = msgspec_default,
|
|
47
47
|
**kwargs,
|
|
48
48
|
) -> None:
|
|
49
49
|
"""
|
|
@@ -54,10 +54,10 @@ class MsgspecFormatter(core.BaseJsonFormatter):
|
|
|
54
54
|
"""
|
|
55
55
|
super().__init__(*args, **kwargs)
|
|
56
56
|
|
|
57
|
-
self.json_default =
|
|
57
|
+
self.json_default = json_default
|
|
58
58
|
self._encoder = msgspec.json.Encoder(enc_hook=self.json_default)
|
|
59
59
|
return
|
|
60
60
|
|
|
61
|
-
def jsonify_log_record(self,
|
|
62
|
-
"""Returns a json string of the log
|
|
63
|
-
return self._encoder.encode(
|
|
61
|
+
def jsonify_log_record(self, log_data: core.LogData) -> str:
|
|
62
|
+
"""Returns a json string of the log data."""
|
|
63
|
+
return self._encoder.encode(log_data).decode("utf8")
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
## Standard Library
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, Optional, Callable
|
|
10
10
|
|
|
11
11
|
## Installed
|
|
12
12
|
|
|
@@ -45,7 +45,7 @@ class OrjsonFormatter(core.BaseJsonFormatter):
|
|
|
45
45
|
def __init__(
|
|
46
46
|
self,
|
|
47
47
|
*args,
|
|
48
|
-
json_default:
|
|
48
|
+
json_default: Optional[Callable] = orjson_default,
|
|
49
49
|
json_indent: bool = False,
|
|
50
50
|
**kwargs,
|
|
51
51
|
) -> None:
|
|
@@ -58,14 +58,14 @@ class OrjsonFormatter(core.BaseJsonFormatter):
|
|
|
58
58
|
"""
|
|
59
59
|
super().__init__(*args, **kwargs)
|
|
60
60
|
|
|
61
|
-
self.json_default =
|
|
61
|
+
self.json_default = json_default
|
|
62
62
|
self.json_indent = json_indent
|
|
63
63
|
return
|
|
64
64
|
|
|
65
|
-
def jsonify_log_record(self,
|
|
66
|
-
"""Returns a json string of the log
|
|
65
|
+
def jsonify_log_record(self, log_data: core.LogData) -> str:
|
|
66
|
+
"""Returns a json string of the log data."""
|
|
67
67
|
opt = orjson.OPT_NON_STR_KEYS
|
|
68
68
|
if self.json_indent:
|
|
69
69
|
opt |= orjson.OPT_INDENT_2
|
|
70
70
|
|
|
71
|
-
return orjson.dumps(
|
|
71
|
+
return orjson.dumps(log_data, default=self.json_default, option=opt).decode("utf8")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
### IMPORTS
|
|
2
|
+
### ============================================================================
|
|
3
|
+
## Future
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
## Standard Library
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
import io
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import logging.config
|
|
12
|
+
from typing import Any, Generator
|
|
13
|
+
|
|
14
|
+
## Installed
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
### SETUP
|
|
18
|
+
### ============================================================================
|
|
19
|
+
_LOGGER_COUNT = 0
|
|
20
|
+
EXT_VAL = 999
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Dummy:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def my_json_default(obj: Any) -> Any:
|
|
28
|
+
if isinstance(obj, Dummy):
|
|
29
|
+
return "DUMMY"
|
|
30
|
+
return obj
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
LOGGING_CONFIG = {
|
|
34
|
+
"version": 1,
|
|
35
|
+
"disable_existing_loggers": False,
|
|
36
|
+
"formatters": {
|
|
37
|
+
"default": {
|
|
38
|
+
"()": "pythonjsonlogger.json.JsonFormatter",
|
|
39
|
+
"json_default": "ext://tests.test_dictconfig.my_json_default",
|
|
40
|
+
"static_fields": {"ext-val": "ext://tests.test_dictconfig.EXT_VAL"},
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"handlers": {
|
|
44
|
+
"default": {
|
|
45
|
+
"level": "DEBUG",
|
|
46
|
+
"formatter": "default",
|
|
47
|
+
"class": "logging.StreamHandler",
|
|
48
|
+
"stream": "ext://sys.stdout", # Default is stderr
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
"loggers": {
|
|
52
|
+
"": {"handlers": ["default"], "level": "WARNING", "propagate": False}, # root logger
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class LoggingEnvironment:
|
|
59
|
+
logger: logging.Logger
|
|
60
|
+
buffer: io.StringIO
|
|
61
|
+
|
|
62
|
+
def load_json(self) -> Any:
|
|
63
|
+
return json.loads(self.buffer.getvalue())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.fixture
|
|
67
|
+
def env() -> Generator[LoggingEnvironment, None, None]:
|
|
68
|
+
global _LOGGER_COUNT # pylint: disable=global-statement
|
|
69
|
+
_LOGGER_COUNT += 1
|
|
70
|
+
logging.config.dictConfig(LOGGING_CONFIG)
|
|
71
|
+
default_formatter = logging.root.handlers[0].formatter
|
|
72
|
+
logger = logging.getLogger(f"pythonjsonlogger.tests.{_LOGGER_COUNT}")
|
|
73
|
+
logger.setLevel(logging.DEBUG)
|
|
74
|
+
buffer = io.StringIO()
|
|
75
|
+
handler = logging.StreamHandler(buffer)
|
|
76
|
+
handler.setFormatter(default_formatter)
|
|
77
|
+
logger.addHandler(handler)
|
|
78
|
+
yield LoggingEnvironment(logger=logger, buffer=buffer)
|
|
79
|
+
logger.removeHandler(handler)
|
|
80
|
+
logger.setLevel(logging.NOTSET)
|
|
81
|
+
buffer.close()
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
### TESTS
|
|
86
|
+
### ============================================================================
|
|
87
|
+
def test_external_reference_support(env: LoggingEnvironment):
|
|
88
|
+
|
|
89
|
+
assert logging.root.handlers[0].formatter.json_default is my_json_default # type: ignore[union-attr]
|
|
90
|
+
|
|
91
|
+
env.logger.info("hello", extra={"dummy": Dummy()})
|
|
92
|
+
log_json = env.load_json()
|
|
93
|
+
|
|
94
|
+
assert log_json["ext-val"] == EXT_VAL
|
|
95
|
+
assert log_json["dummy"] == "DUMMY"
|
|
96
|
+
return
|
|
@@ -158,12 +158,48 @@ def test_default_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]
|
|
|
158
158
|
|
|
159
159
|
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
|
|
160
160
|
def test_percentage_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
161
|
+
# Note: We use different %s styles in the format to check the regex correctly collects them
|
|
162
|
+
env.set_formatter(class_("[%(levelname)8s] %(message)s %(filename)s:%(lineno)d %(asctime)"))
|
|
163
|
+
|
|
164
|
+
msg = "testing logging format"
|
|
165
|
+
env.logger.info(msg)
|
|
166
|
+
log_json = env.load_json()
|
|
167
|
+
|
|
168
|
+
assert log_json["message"] == msg
|
|
169
|
+
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
|
|
174
|
+
def test_comma_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
175
|
+
# Note: we have double comma `,,` to test handling "empty" names
|
|
176
|
+
env.set_formatter(class_("levelname,,message,filename,lineno,asctime,", style=","))
|
|
177
|
+
|
|
178
|
+
msg = "testing logging format"
|
|
179
|
+
env.logger.info(msg)
|
|
180
|
+
log_json = env.load_json()
|
|
181
|
+
|
|
182
|
+
assert log_json["message"] == msg
|
|
183
|
+
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
|
|
188
|
+
def test_sequence_list_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
189
|
+
env.set_formatter(class_(["levelname", "message", "filename", "lineno", "asctime"]))
|
|
190
|
+
|
|
191
|
+
msg = "testing logging format"
|
|
192
|
+
env.logger.info(msg)
|
|
193
|
+
log_json = env.load_json()
|
|
194
|
+
|
|
195
|
+
assert log_json["message"] == msg
|
|
196
|
+
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
|
|
201
|
+
def test_sequence_tuple_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
202
|
+
env.set_formatter(class_(("levelname", "message", "filename", "lineno", "asctime")))
|
|
167
203
|
|
|
168
204
|
msg = "testing logging format"
|
|
169
205
|
env.logger.info(msg)
|
|
@@ -380,9 +416,9 @@ def test_log_extra(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
|
380
416
|
def test_custom_logic_adds_field(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
|
|
381
417
|
class CustomJsonFormatter(class_): # type: ignore[valid-type,misc]
|
|
382
418
|
|
|
383
|
-
def process_log_record(self,
|
|
384
|
-
|
|
385
|
-
return super().process_log_record(
|
|
419
|
+
def process_log_record(self, log_data):
|
|
420
|
+
log_data["custom"] = "value"
|
|
421
|
+
return super().process_log_record(log_data)
|
|
386
422
|
|
|
387
423
|
env.set_formatter(CustomJsonFormatter())
|
|
388
424
|
env.logger.info("message")
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|