python-json-logger 3.2.1.dev1__tar.gz → 4.0.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.
Files changed (27) hide show
  1. {python_json_logger-3.2.1.dev1/src/python_json_logger.egg-info → python_json_logger-4.0.0}/PKG-INFO +5 -4
  2. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/README.md +1 -0
  3. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/pyproject.toml +2 -3
  4. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0/src/python_json_logger.egg-info}/PKG-INFO +5 -4
  5. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/python_json_logger.egg-info/SOURCES.txt +1 -0
  6. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/python_json_logger.egg-info/requires.txt +0 -5
  7. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/pythonjsonlogger/core.py +120 -71
  8. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/pythonjsonlogger/json.py +9 -9
  9. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/pythonjsonlogger/msgspec.py +6 -6
  10. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/pythonjsonlogger/orjson.py +6 -6
  11. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/tests/test_deprecation.py +17 -0
  12. python_json_logger-4.0.0/tests/test_dictconfig.py +96 -0
  13. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/tests/test_formatters.py +102 -11
  14. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/LICENSE +0 -0
  15. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/MANIFEST.in +0 -0
  16. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/NOTICE +0 -0
  17. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/setup.cfg +0 -0
  18. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/python_json_logger.egg-info/dependency_links.txt +0 -0
  19. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/python_json_logger.egg-info/top_level.txt +0 -0
  20. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/pythonjsonlogger/__init__.py +0 -0
  21. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/pythonjsonlogger/defaults.py +0 -0
  22. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/pythonjsonlogger/exception.py +0 -0
  23. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/pythonjsonlogger/jsonlogger.py +0 -0
  24. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/pythonjsonlogger/py.typed +0 -0
  25. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/src/pythonjsonlogger/utils.py +0 -0
  26. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/tests/__init__.py +0 -0
  27. {python_json_logger-3.2.1.dev1 → python_json_logger-4.0.0}/tests/test_missing.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-json-logger
3
- Version: 3.2.1.dev1
3
+ Version: 4.0.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>
@@ -27,8 +27,7 @@ License-File: NOTICE
27
27
  Requires-Dist: typing_extensions; python_version < "3.10"
28
28
  Provides-Extra: dev
29
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"
30
+ Requires-Dist: msgspec; implementation_name != "pypy" and extra == "dev"
32
31
  Requires-Dist: validate-pyproject[all]; extra == "dev"
33
32
  Requires-Dist: black; extra == "dev"
34
33
  Requires-Dist: pylint; extra == "dev"
@@ -46,9 +45,11 @@ Requires-Dist: mkdocstrings[python]; extra == "dev"
46
45
  Requires-Dist: mkdocs-gen-files; extra == "dev"
47
46
  Requires-Dist: mkdocs-literate-nav; extra == "dev"
48
47
  Requires-Dist: mike; extra == "dev"
48
+ Dynamic: license-file
49
49
 
50
50
  [![PyPi](https://img.shields.io/pypi/v/python-json-logger.svg)](https://pypi.python.org/pypi/python-json-logger/)
51
51
  [![PyPI - Status](https://img.shields.io/pypi/status/python-json-logger)](https://pypi.python.org/pypi/python-json-logger/)
52
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/python-json-logger)](https://pypi.python.org/pypi/python-json-logger/)
52
53
  [![Python Versions](https://img.shields.io/pypi/pyversions/python-json-logger.svg)](https://github.com/nhairs/python-json-logger)
53
54
  [![License](https://img.shields.io/github/license/nhairs/python-json-logger.svg)](https://github.com/nhairs/python-json-logger)
54
55
  ![Build Status](https://github.com/nhairs/python-json-logger/actions/workflows/test-suite.yml/badge.svg)
@@ -1,5 +1,6 @@
1
1
  [![PyPi](https://img.shields.io/pypi/v/python-json-logger.svg)](https://pypi.python.org/pypi/python-json-logger/)
2
2
  [![PyPI - Status](https://img.shields.io/pypi/status/python-json-logger)](https://pypi.python.org/pypi/python-json-logger/)
3
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/python-json-logger)](https://pypi.python.org/pypi/python-json-logger/)
3
4
  [![Python Versions](https://img.shields.io/pypi/pyversions/python-json-logger.svg)](https://github.com/nhairs/python-json-logger)
4
5
  [![License](https://img.shields.io/github/license/nhairs/python-json-logger.svg)](https://github.com/nhairs/python-json-logger)
5
6
  ![Build Status](https://github.com/nhairs/python-json-logger/actions/workflows/test-suite.yml/badge.svg)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-json-logger"
7
- version = "3.2.1.dev1"
7
+ version = "4.0.0"
8
8
  description = "JSON Log Formatter for the Python Logging Package"
9
9
  authors = [
10
10
  {name = "Zakaria Zajac", email = "zak@madzak.com"},
@@ -47,8 +47,7 @@ GitHub = "https://github.com/nhairs/python-json-logger"
47
47
  dev = [
48
48
  ## Optional but required for dev
49
49
  "orjson;implementation_name!='pypy'",
50
- "msgspec;implementation_name!='pypy' and python_version<'3.13'",
51
- "msgspec-python313-pre;implementation_name!='pypy' and python_version=='3.13'",
50
+ "msgspec;implementation_name!='pypy'",
52
51
  ## Lint
53
52
  "validate-pyproject[all]",
54
53
  "black",
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-json-logger
3
- Version: 3.2.1.dev1
3
+ Version: 4.0.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>
@@ -27,8 +27,7 @@ License-File: NOTICE
27
27
  Requires-Dist: typing_extensions; python_version < "3.10"
28
28
  Provides-Extra: dev
29
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"
30
+ Requires-Dist: msgspec; implementation_name != "pypy" and extra == "dev"
32
31
  Requires-Dist: validate-pyproject[all]; extra == "dev"
33
32
  Requires-Dist: black; extra == "dev"
34
33
  Requires-Dist: pylint; extra == "dev"
@@ -46,9 +45,11 @@ Requires-Dist: mkdocstrings[python]; extra == "dev"
46
45
  Requires-Dist: mkdocs-gen-files; extra == "dev"
47
46
  Requires-Dist: mkdocs-literate-nav; extra == "dev"
48
47
  Requires-Dist: mike; extra == "dev"
48
+ Dynamic: license-file
49
49
 
50
50
  [![PyPi](https://img.shields.io/pypi/v/python-json-logger.svg)](https://pypi.python.org/pypi/python-json-logger/)
51
51
  [![PyPI - Status](https://img.shields.io/pypi/status/python-json-logger)](https://pypi.python.org/pypi/python-json-logger/)
52
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/python-json-logger)](https://pypi.python.org/pypi/python-json-logger/)
52
53
  [![Python Versions](https://img.shields.io/pypi/pyversions/python-json-logger.svg)](https://github.com/nhairs/python-json-logger)
53
54
  [![License](https://img.shields.io/github/license/nhairs/python-json-logger.svg)](https://github.com/nhairs/python-json-logger)
54
55
  ![Build Status](https://github.com/nhairs/python-json-logger/actions/workflows/test-suite.yml/badge.svg)
@@ -20,5 +20,6 @@ src/pythonjsonlogger/py.typed
20
20
  src/pythonjsonlogger/utils.py
21
21
  tests/__init__.py
22
22
  tests/test_deprecation.py
23
+ tests/test_dictconfig.py
23
24
  tests/test_formatters.py
24
25
  tests/test_missing.py
@@ -22,12 +22,7 @@ mike
22
22
 
23
23
  [dev:implementation_name != "pypy"]
24
24
  orjson
25
-
26
- [dev:implementation_name != "pypy" and python_version < "3.13"]
27
25
  msgspec
28
26
 
29
- [dev:implementation_name != "pypy" and python_version == "3.13"]
30
- msgspec-python313-pre
31
-
32
27
  [dev:python_version < "3.9"]
33
28
  backports.zoneinfo
@@ -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, Callable, List, Dict, Container, Any, Sequence
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
- OptionalCallableOrStr: TypeAlias = Optional[Union[Callable, str]]
76
- """Type alias"""
74
+ LogData: TypeAlias = Dict[str, Any]
75
+ """Type alias
77
76
 
78
- LogRecord: TypeAlias = Dict[str, Any]
79
- """Type alias"""
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,
@@ -134,6 +117,8 @@ class BaseJsonFormatter(logging.Formatter):
134
117
  *New in 3.1*
135
118
 
136
119
  *Changed in 3.2*: `defaults` argument is no longer ignored.
120
+
121
+ *Added in 3.3*: `exc_info_as_array` and `stack_info_as_array` options are added.
137
122
  """
138
123
 
139
124
  _style: Union[logging.PercentStyle, str] # type: ignore[assignment]
@@ -143,7 +128,7 @@ class BaseJsonFormatter(logging.Formatter):
143
128
  # pylint: disable=too-many-arguments,super-init-not-called
144
129
  def __init__(
145
130
  self,
146
- fmt: Optional[str] = None,
131
+ fmt: Optional[Union[str, Sequence[str]]] = None,
147
132
  datefmt: Optional[str] = None,
148
133
  style: str = "%",
149
134
  validate: bool = True,
@@ -155,14 +140,16 @@ class BaseJsonFormatter(logging.Formatter):
155
140
  reserved_attrs: Optional[Sequence[str]] = None,
156
141
  timestamp: Union[bool, str] = False,
157
142
  defaults: Optional[Dict[str, Any]] = None,
143
+ exc_info_as_array: bool = False,
144
+ stack_info_as_array: bool = False,
158
145
  ) -> None:
159
146
  """
160
147
  Args:
161
- fmt: string representing fields to log
148
+ fmt: String format or `Sequence` of field names of fields to log.
162
149
  datefmt: format to use when formatting `asctime` field
163
- style: how to extract log fields from `fmt`
150
+ style: how to extract log fields from `fmt`. Ignored if `fmt` is a `Sequence[str]`.
164
151
  validate: validate `fmt` against style, if implementing a custom `style` you
165
- must set this to `False`.
152
+ must set this to `False`. Ignored if `fmt` is a `Sequence[str]`.
166
153
  defaults: a dictionary containing default fields that are added before all other fields and
167
154
  may be overridden. The supplied fields are still subject to `rename_fields`.
168
155
  prefix: an optional string prefix added at the beginning of
@@ -177,6 +164,8 @@ class BaseJsonFormatter(logging.Formatter):
177
164
  outputting the json log record. If string is passed, timestamp will be added
178
165
  to log record using string as key. If True boolean is passed, timestamp key
179
166
  will be "timestamp". Defaults to False/off.
167
+ exc_info_as_array: break the exc_info into a list of lines based on line breaks.
168
+ stack_info_as_array: break the stack_info into a list of lines based on line breaks.
180
169
 
181
170
  *Changed in 3.1*:
182
171
 
@@ -186,39 +175,66 @@ class BaseJsonFormatter(logging.Formatter):
186
175
  - Renaming fields now preserves the order that fields were added in and avoids adding
187
176
  missing fields. The original behaviour, missing fields have a value of `None`, is still
188
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.
189
185
  """
190
186
  ## logging.Formatter compatibility
191
187
  ## ---------------------------------------------------------------------
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())}")
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)
206
212
 
207
213
  self.datefmt = datefmt
208
214
 
209
215
  ## JSON Logging specific
210
216
  ## ---------------------------------------------------------------------
211
217
  self.prefix = prefix
212
- self.rename_fields = rename_fields if rename_fields is not None else {}
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
+
213
229
  self.rename_fields_keep_missing = rename_fields_keep_missing
214
- self.static_fields = static_fields if static_fields is not None else {}
215
230
  self.reserved_attrs = set(reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS)
216
231
  self.timestamp = timestamp
217
232
 
218
- self._required_fields = self.parse()
219
233
  self._skip_fields = set(self._required_fields)
220
234
  self._skip_fields.update(self.reserved_attrs)
221
235
  self.defaults = defaults if defaults is not None else {}
236
+ self.exc_info_as_array = exc_info_as_array
237
+ self.stack_info_as_array = stack_info_as_array
222
238
  return
223
239
 
224
240
  def format(self, record: logging.LogRecord) -> str:
@@ -252,11 +268,11 @@ class BaseJsonFormatter(logging.Formatter):
252
268
  if record.stack_info and not message_dict.get("stack_info"):
253
269
  message_dict["stack_info"] = self.formatStack(record.stack_info)
254
270
 
255
- log_record: LogRecord = {}
256
- self.add_fields(log_record, record, message_dict)
257
- log_record = self.process_log_record(log_record)
271
+ log_data: LogData = {}
272
+ self.add_fields(log_data, record, message_dict)
273
+ log_data = self.process_log_record(log_data)
258
274
 
259
- return self.serialize_log_record(log_record)
275
+ return self.serialize_log_record(log_data)
260
276
 
261
277
  ## JSON Formatter Specific Methods
262
278
  ## -------------------------------------------------------------------------
@@ -271,6 +287,18 @@ class BaseJsonFormatter(logging.Formatter):
271
287
  Returns:
272
288
  list of fields to be extracted and serialized
273
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
+
274
302
  if isinstance(self._style, logging.StringTemplateStyle):
275
303
  formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX
276
304
 
@@ -285,22 +313,21 @@ class BaseJsonFormatter(logging.Formatter):
285
313
  else:
286
314
  raise ValueError(f"Style {self._style!r} is not supported")
287
315
 
288
- if self._fmt:
289
- return formatter_style_pattern.findall(self._fmt)
290
-
291
- return []
316
+ return formatter_style_pattern.findall(self._fmt)
292
317
 
293
- def serialize_log_record(self, log_record: LogRecord) -> str:
294
- """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
295
320
 
296
321
  Args:
297
- log_record: the log record
322
+ log_data: the data
323
+
324
+ *Changed in 4.0*: `log_record` renamed to `log_data`
298
325
  """
299
- return self.prefix + self.jsonify_log_record(log_record)
326
+ return self.prefix + self.jsonify_log_record(log_data)
300
327
 
301
328
  def add_fields(
302
329
  self,
303
- log_record: Dict[str, Any],
330
+ log_data: Dict[str, Any],
304
331
  record: logging.LogRecord,
305
332
  message_dict: Dict[str, Any],
306
333
  ) -> None:
@@ -309,38 +336,40 @@ class BaseJsonFormatter(logging.Formatter):
309
336
  This method can be overridden to implement custom logic for adding fields.
310
337
 
311
338
  Args:
312
- log_record: data that will be logged
339
+ log_data: data that will be logged
313
340
  record: the record to extract data from
314
341
  message_dict: dictionary that was logged instead of a message. e.g
315
342
  `logger.info({"is_this_message_dict": True})`
343
+
344
+ *Changed in 4.0*: `log_record` renamed to `log_data`
316
345
  """
317
346
  for field in self.defaults:
318
- log_record[self._get_rename(field)] = self.defaults[field]
347
+ log_data[self._get_rename(field)] = self.defaults[field]
319
348
 
320
349
  for field in self._required_fields:
321
- log_record[self._get_rename(field)] = record.__dict__.get(field)
350
+ log_data[self._get_rename(field)] = record.__dict__.get(field)
322
351
 
323
352
  for data_dict in [self.static_fields, message_dict]:
324
353
  for key, value in data_dict.items():
325
- log_record[self._get_rename(key)] = value
354
+ log_data[self._get_rename(key)] = value
326
355
 
327
356
  merge_record_extra(
328
357
  record,
329
- log_record,
358
+ log_data,
330
359
  reserved=self._skip_fields,
331
360
  rename_fields=self.rename_fields,
332
361
  )
333
362
 
334
363
  if self.timestamp:
335
364
  key = self.timestamp if isinstance(self.timestamp, str) else "timestamp"
336
- log_record[self._get_rename(key)] = datetime.fromtimestamp(
365
+ log_data[self._get_rename(key)] = datetime.fromtimestamp(
337
366
  record.created, tz=timezone.utc
338
367
  )
339
368
 
340
369
  if self.rename_fields_keep_missing:
341
370
  for field in self.rename_fields.values():
342
- if field not in log_record:
343
- log_record[field] = None
371
+ if field not in log_data:
372
+ log_data[field] = None
344
373
  return
345
374
 
346
375
  def _get_rename(self, key: str) -> str:
@@ -348,23 +377,43 @@ class BaseJsonFormatter(logging.Formatter):
348
377
 
349
378
  # Child Methods
350
379
  # ..........................................................................
351
- def jsonify_log_record(self, log_record: LogRecord) -> str:
352
- """Convert this log record into a JSON string.
380
+ def jsonify_log_record(self, log_data: LogData) -> str:
381
+ """Convert the log data into a JSON string.
353
382
 
354
383
  Child classes MUST override this method.
355
384
 
356
385
  Args:
357
- log_record: the data to serialize
386
+ log_data: the data to serialize
387
+
388
+ *Changed in 4.0*: `log_record` renamed to `log_data`
358
389
  """
359
390
  raise NotImplementedError()
360
391
 
361
- def process_log_record(self, log_record: LogRecord) -> LogRecord:
362
- """Custom processing of the log record.
392
+ def process_log_record(self, log_data: LogData) -> LogData:
393
+ """Custom processing of the data to be logged.
363
394
 
364
395
  Child classes can override this method to alter the log record before it
365
396
  is serialized.
366
397
 
367
398
  Args:
368
- log_record: incoming data
399
+ log_data: incoming data
400
+
401
+ *Changed in 4.0*: `log_record` renamed to `log_data`
402
+ """
403
+ return log_data
404
+
405
+ def formatException(self, ei) -> Union[str, list[str]]: # type: ignore
406
+ """Format and return the specified exception information.
407
+
408
+ If exc_info_as_array is set to True, This method returns an array of strings.
409
+ """
410
+ exception_info_str = super().formatException(ei)
411
+ return exception_info_str.splitlines() if self.exc_info_as_array else exception_info_str
412
+
413
+ def formatStack(self, stack_info) -> Union[str, list[str]]: # type: ignore
414
+ """Format and return the specified stack information.
415
+
416
+ If stack_info_as_array is set to True, This method returns an array of strings.
369
417
  """
370
- return log_record
418
+ stack_info_str = super().formatStack(stack_info)
419
+ return stack_info_str.splitlines() if self.stack_info_as_array else stack_info_str
@@ -67,9 +67,9 @@ class JsonFormatter(core.BaseJsonFormatter):
67
67
  def __init__(
68
68
  self,
69
69
  *args,
70
- json_default: core.OptionalCallableOrStr = None,
71
- json_encoder: core.OptionalCallableOrStr = None,
72
- json_serializer: Union[Callable, str] = json.dumps,
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 = core.str_to_object(json_default)
91
- self.json_encoder = core.str_to_object(json_encoder)
92
- self.json_serializer = core.str_to_object(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, log_record: core.LogRecord) -> str:
100
- """Returns a json string of the log record."""
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
- log_record,
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: core.OptionalCallableOrStr = msgspec_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 = core.str_to_object(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, log_record: core.LogRecord) -> str:
62
- """Returns a json string of the log record."""
63
- return self._encoder.encode(log_record).decode("utf8")
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: core.OptionalCallableOrStr = orjson_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 = core.str_to_object(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, log_record: core.LogRecord) -> str:
66
- """Returns a json string of the log record."""
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(log_record, default=self.json_default, option=opt).decode("utf8")
71
+ return orjson.dumps(log_data, default=self.json_default, option=opt).decode("utf8")
@@ -4,6 +4,8 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  ## Standard Library
7
+ import subprocess
8
+ import sys
7
9
 
8
10
  ## Installed
9
11
  import pytest
@@ -26,3 +28,18 @@ def test_jsonlogger_reserved_attrs_deprecated():
26
28
  # a DeprecationWarning and we specifically want the one for RESERVED_ATTRS
27
29
  pythonjsonlogger.json.RESERVED_ATTRS
28
30
  return
31
+
32
+
33
+ @pytest.mark.parametrize(
34
+ "command",
35
+ [
36
+ "from pythonjsonlogger import jsonlogger",
37
+ "import pythonjsonlogger.jsonlogger",
38
+ "from pythonjsonlogger.jsonlogger import JsonFormatter",
39
+ "from pythonjsonlogger.jsonlogger import RESERVED_ATTRS",
40
+ ],
41
+ )
42
+ def test_import(command: str):
43
+ output = subprocess.check_output([sys.executable, "-c", f"{command};print('OK')"])
44
+ assert output.strip() == b"OK"
45
+ return
@@ -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
- env.set_formatter(
162
- class_(
163
- # All kind of different styles to check the regex
164
- "[%(levelname)8s] %(message)s %(filename)s:%(lineno)d %(asctime)"
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, log_record):
384
- log_record["custom"] = "value"
385
- return super().process_log_record(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")
@@ -568,8 +604,14 @@ def test_common_types_encoded(
568
604
  if pythonjsonlogger.MSGSPEC_AVAILABLE and class_ is MsgspecFormatter:
569
605
  # Dataclass: https://github.com/jcrist/msgspec/issues/681
570
606
  # Enum: https://github.com/jcrist/msgspec/issues/680
571
- if obj is SomeDataclass or (
572
- isinstance(obj, enum.Enum) and obj in {MultiEnum.BYTES, MultiEnum.NONE, MultiEnum.BOOL}
607
+ # These have been fixed in msgspec 0.19.0, however they also dropped python 3.8 support.
608
+ # https://github.com/jcrist/msgspec/releases/tag/0.19.0
609
+ if sys.version_info < (3, 9) and (
610
+ obj is SomeDataclass
611
+ or (
612
+ isinstance(obj, enum.Enum)
613
+ and obj in {MultiEnum.BYTES, MultiEnum.NONE, MultiEnum.BOOL}
614
+ )
573
615
  ):
574
616
  pytest.xfail()
575
617
 
@@ -616,6 +658,55 @@ def test_custom_default(env: LoggingEnvironment, class_: type[BaseJsonFormatter]
616
658
  return
617
659
 
618
660
 
661
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
662
+ def test_exc_info_as_array(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
663
+ env.set_formatter(class_(exc_info_as_array=True))
664
+
665
+ try:
666
+ raise Exception("Error")
667
+ except BaseException:
668
+ env.logger.exception("Error occurs")
669
+ log_json = env.load_json()
670
+
671
+ assert isinstance(log_json["exc_info"], list)
672
+ return
673
+
674
+
675
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
676
+ def test_exc_info_as_array_no_exc_info(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
677
+ env.set_formatter(class_(exc_info_as_array=True))
678
+
679
+ env.logger.info("hello")
680
+ log_json = env.load_json()
681
+
682
+ assert "exc_info" not in log_json
683
+ return
684
+
685
+
686
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
687
+ def test_stack_info_as_array(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
688
+ env.set_formatter(class_(stack_info_as_array=True))
689
+
690
+ env.logger.info("hello", stack_info=True)
691
+ log_json = env.load_json()
692
+
693
+ assert isinstance(log_json["stack_info"], list)
694
+ return
695
+
696
+
697
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
698
+ def test_stack_info_as_array_no_stack_info(
699
+ env: LoggingEnvironment, class_: type[BaseJsonFormatter]
700
+ ):
701
+ env.set_formatter(class_(stack_info_as_array=True))
702
+
703
+ env.logger.info("hello", stack_info=False)
704
+ log_json = env.load_json()
705
+
706
+ assert "stack_info" not in log_json
707
+ return
708
+
709
+
619
710
  ## JsonFormatter Specific
620
711
  ## -----------------------------------------------------------------------------
621
712
  def test_json_ensure_ascii_true(env: LoggingEnvironment):