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.
Files changed (27) hide show
  1. {python_json_logger-4.0.0.dev0/src/python_json_logger.egg-info → python_json_logger-4.1.0}/PKG-INFO +4 -9
  2. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/pyproject.toml +5 -10
  3. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0/src/python_json_logger.egg-info}/PKG-INFO +4 -9
  4. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/python_json_logger.egg-info/requires.txt +0 -6
  5. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/core.py +103 -91
  6. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/defaults.py +1 -7
  7. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/json.py +12 -11
  8. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/msgspec.py +6 -5
  9. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/orjson.py +7 -6
  10. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/tests/test_dictconfig.py +20 -3
  11. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/tests/test_formatters.py +50 -16
  12. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/LICENSE +0 -0
  13. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/MANIFEST.in +0 -0
  14. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/NOTICE +0 -0
  15. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/README.md +0 -0
  16. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/setup.cfg +0 -0
  17. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/python_json_logger.egg-info/SOURCES.txt +0 -0
  18. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/python_json_logger.egg-info/dependency_links.txt +0 -0
  19. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/python_json_logger.egg-info/top_level.txt +0 -0
  20. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/__init__.py +0 -0
  21. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/exception.py +0 -0
  22. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/jsonlogger.py +0 -0
  23. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/py.typed +0 -0
  24. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/src/pythonjsonlogger/utils.py +0 -0
  25. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/tests/__init__.py +0 -0
  26. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/tests/test_deprecation.py +0 -0
  27. {python_json_logger-4.0.0.dev0 → python_json_logger-4.1.0}/tests/test_missing.py +0 -0
@@ -1,30 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-json-logger
3
- Version: 4.0.0.dev0
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 License
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.8
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.0.0.dev0"
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.8"
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 = {text = "BSD-2-Clause 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",
@@ -1,30 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-json-logger
3
- Version: 4.0.0.dev0
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 License
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.8
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"
@@ -1,7 +1,4 @@
1
1
 
2
- [:python_version < "3.10"]
3
- typing_extensions
4
-
5
2
  [dev]
6
3
  validate-pyproject[all]
7
4
  black
@@ -23,6 +20,3 @@ mike
23
20
  [dev:implementation_name != "pypy"]
24
21
  orjson
25
22
  msgspec
26
-
27
- [dev:python_version < "3.9"]
28
- backports.zoneinfo
@@ -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 Optional, Union, Callable, List, Dict, Container, Any, Sequence
15
-
16
- if sys.version_info >= (3, 10):
17
- from typing import TypeAlias
18
- else:
19
- from typing_extensions import TypeAlias
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: List[str] = [
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
- OptionalCallableOrStr: TypeAlias = Optional[Union[Callable, str]]
76
- """Type alias"""
70
+ LogData: TypeAlias = dict[str, Any]
71
+ """Type alias
77
72
 
78
- LogRecord: TypeAlias = Dict[str, Any]
79
- """Type alias"""
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: Dict,
81
+ target: dict[Any, Any],
103
82
  reserved: Container[str],
104
- rename_fields: Optional[Dict[str, str]] = None,
105
- ) -> Dict:
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 UNRELEASED*: `exc_info_as_array` and `stack_info_as_array` options are added.
117
+ *Added in 3.3*: `exc_info_as_array` and `stack_info_as_array` options are added.
139
118
  """
140
119
 
141
- _style: Union[logging.PercentStyle, str] # type: ignore[assignment]
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: Optional[str] = None,
149
- datefmt: Optional[str] = None,
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: Optional[Dict[str, str]] = None,
133
+ rename_fields: dict[str, str] | None = None,
155
134
  rename_fields_keep_missing: bool = False,
156
- static_fields: Optional[Dict[str, Any]] = None,
157
- reserved_attrs: Optional[Sequence[str]] = None,
158
- timestamp: Union[bool, str] = False,
159
- defaults: Optional[Dict[str, Any]] = None,
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: string representing fields to log
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 style in logging._STYLES:
200
- _style = logging._STYLES[style][0](fmt) # type: ignore[operator]
201
- if validate:
202
- _style.validate()
203
- self._style = _style
204
- self._fmt = _style._fmt
205
-
206
- elif not validate:
207
- self._style = style
208
- self._fmt = fmt
209
-
210
- else:
211
- raise ValueError(f"Style must be one of: {','.join(logging._STYLES.keys())}")
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: Dict[str, Any] = {}
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
- log_record: LogRecord = {}
273
- self.add_fields(log_record, record, message_dict)
274
- log_record = self.process_log_record(log_record)
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(log_record)
271
+ return self.serialize_log_record(log_data)
277
272
 
278
273
  ## JSON Formatter Specific Methods
279
274
  ## -------------------------------------------------------------------------
280
- def parse(self) -> List[str]:
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
- if self._fmt:
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, log_record: LogRecord) -> str:
311
- """Returns the final representation of the log record.
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
- log_record: the log record
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(log_record)
322
+ return self.prefix + self.jsonify_log_record(log_data)
317
323
 
318
324
  def add_fields(
319
325
  self,
320
- log_record: Dict[str, Any],
326
+ log_data: dict[str, Any],
321
327
  record: logging.LogRecord,
322
- message_dict: Dict[str, Any],
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
- log_record: data that will be logged
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
- log_record[self._get_rename(field)] = self.defaults[field]
343
+ log_data[self._get_rename(field)] = self.defaults[field]
336
344
 
337
345
  for field in self._required_fields:
338
- log_record[self._get_rename(field)] = record.__dict__.get(field)
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
- log_record[self._get_rename(key)] = value
350
+ log_data[self._get_rename(key)] = value
343
351
 
344
352
  merge_record_extra(
345
353
  record,
346
- log_record,
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
- log_record[self._get_rename(key)] = datetime.fromtimestamp(
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 log_record:
360
- log_record[field] = None
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, log_record: LogRecord) -> str:
369
- """Convert this log record into a JSON string.
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
- log_record: the data to serialize
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, log_record: LogRecord) -> LogRecord:
379
- """Custom processing of the log record.
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
- log_record: incoming data
395
+ log_data: incoming data
396
+
397
+ *Changed in 4.0*: `log_record` renamed to `log_data`
386
398
  """
387
- return log_record
399
+ return log_data
388
400
 
389
- def formatException(self, ei) -> Union[str, list[str]]: # type: ignore
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) -> Union[str, list[str]]: # type: ignore
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, Callable, Optional, Union
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: core.OptionalCallableOrStr = None,
71
- json_encoder: core.OptionalCallableOrStr = None,
72
- json_serializer: Union[Callable, str] = json.dumps,
73
- json_indent: Optional[Union[int, str]] = None,
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 = 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)
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, log_record: core.LogRecord) -> str:
100
- """Returns a json string of the log record."""
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
- log_record,
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: core.OptionalCallableOrStr = msgspec_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 = core.str_to_object(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, log_record: core.LogRecord) -> str:
62
- """Returns a json string of the log record."""
63
- return self._encoder.encode(log_record).decode("utf8")
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 msgspec after checking it is available
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: core.OptionalCallableOrStr = orjson_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 = core.str_to_object(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, log_record: core.LogRecord) -> str:
66
- """Returns a json string of the log record."""
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(log_record, default=self.json_default, option=opt).decode("utf8")
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, Generator
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, None, None]:
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
- env.logger.info("hello")
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, Generator
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, None, None]:
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
- 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
- )
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, log_record):
384
- log_record["custom"] = "value"
385
- return super().process_log_record(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"),