python-json-logger 3.3.0__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.3.0/src/python_json_logger.egg-info → python_json_logger-4.0.0}/PKG-INFO +3 -2
  2. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/pyproject.toml +1 -1
  3. {python_json_logger-3.3.0 → python_json_logger-4.0.0/src/python_json_logger.egg-info}/PKG-INFO +3 -2
  4. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/python_json_logger.egg-info/SOURCES.txt +1 -0
  5. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/pythonjsonlogger/core.py +97 -72
  6. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/pythonjsonlogger/json.py +9 -9
  7. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/pythonjsonlogger/msgspec.py +6 -6
  8. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/pythonjsonlogger/orjson.py +6 -6
  9. python_json_logger-4.0.0/tests/test_dictconfig.py +96 -0
  10. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/tests/test_formatters.py +45 -9
  11. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/LICENSE +0 -0
  12. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/MANIFEST.in +0 -0
  13. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/NOTICE +0 -0
  14. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/README.md +0 -0
  15. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/setup.cfg +0 -0
  16. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/python_json_logger.egg-info/dependency_links.txt +0 -0
  17. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/python_json_logger.egg-info/requires.txt +0 -0
  18. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/python_json_logger.egg-info/top_level.txt +0 -0
  19. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/pythonjsonlogger/__init__.py +0 -0
  20. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/pythonjsonlogger/defaults.py +0 -0
  21. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/pythonjsonlogger/exception.py +0 -0
  22. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/pythonjsonlogger/jsonlogger.py +0 -0
  23. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/pythonjsonlogger/py.typed +0 -0
  24. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/src/pythonjsonlogger/utils.py +0 -0
  25. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/tests/__init__.py +0 -0
  26. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/tests/test_deprecation.py +0 -0
  27. {python_json_logger-3.3.0 → python_json_logger-4.0.0}/tests/test_missing.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: python-json-logger
3
- Version: 3.3.0
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>
@@ -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
  [![PyPi](https://img.shields.io/pypi/v/python-json-logger.svg)](https://pypi.python.org/pypi/python-json-logger/)
50
51
  [![PyPI - Status](https://img.shields.io/pypi/status/python-json-logger)](https://pypi.python.org/pypi/python-json-logger/)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-json-logger"
7
- version = "3.3.0"
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"},
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: python-json-logger
3
- Version: 3.3.0
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>
@@ -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
  [![PyPi](https://img.shields.io/pypi/v/python-json-logger.svg)](https://pypi.python.org/pypi/python-json-logger/)
50
51
  [![PyPI - Status](https://img.shields.io/pypi/status/python-json-logger)](https://pypi.python.org/pypi/python-json-logger/)
@@ -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
@@ -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,
@@ -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 UNRELEASED*: `exc_info_as_array` and `stack_info_as_array` options are added.
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: string representing fields to log
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 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())}")
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
- 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
+
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
- log_record: LogRecord = {}
264
- self.add_fields(log_record, record, message_dict)
265
- 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)
266
274
 
267
- return self.serialize_log_record(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
- if self._fmt:
297
- return formatter_style_pattern.findall(self._fmt)
316
+ return formatter_style_pattern.findall(self._fmt)
298
317
 
299
- return []
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
- log_record: the log record
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(log_record)
326
+ return self.prefix + self.jsonify_log_record(log_data)
308
327
 
309
328
  def add_fields(
310
329
  self,
311
- log_record: Dict[str, Any],
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
- log_record: data that will be logged
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
- log_record[self._get_rename(field)] = self.defaults[field]
347
+ log_data[self._get_rename(field)] = self.defaults[field]
327
348
 
328
349
  for field in self._required_fields:
329
- log_record[self._get_rename(field)] = record.__dict__.get(field)
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
- log_record[self._get_rename(key)] = value
354
+ log_data[self._get_rename(key)] = value
334
355
 
335
356
  merge_record_extra(
336
357
  record,
337
- log_record,
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
- log_record[self._get_rename(key)] = datetime.fromtimestamp(
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 log_record:
351
- log_record[field] = None
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, log_record: LogRecord) -> str:
360
- """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.
361
382
 
362
383
  Child classes MUST override this method.
363
384
 
364
385
  Args:
365
- 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`
366
389
  """
367
390
  raise NotImplementedError()
368
391
 
369
- def process_log_record(self, log_record: LogRecord) -> LogRecord:
370
- """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.
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
- log_record: incoming data
399
+ log_data: incoming data
400
+
401
+ *Changed in 4.0*: `log_record` renamed to `log_data`
377
402
  """
378
- return log_record
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: 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")
@@ -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")