python-json-logger 4.0.0.dev0__tar.gz → 4.0.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {python_json_logger-4.0.0.dev0/src/python_json_logger.egg-info → python_json_logger-4.0.0rc1}/PKG-INFO +1 -1
  2. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/pyproject.toml +1 -1
  3. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1/src/python_json_logger.egg-info}/PKG-INFO +1 -1
  4. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/core.py +86 -70
  5. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/json.py +9 -9
  6. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/msgspec.py +6 -6
  7. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/orjson.py +6 -6
  8. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/tests/test_dictconfig.py +17 -1
  9. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/tests/test_formatters.py +45 -9
  10. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/LICENSE +0 -0
  11. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/MANIFEST.in +0 -0
  12. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/NOTICE +0 -0
  13. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/README.md +0 -0
  14. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/setup.cfg +0 -0
  15. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/python_json_logger.egg-info/SOURCES.txt +0 -0
  16. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/python_json_logger.egg-info/dependency_links.txt +0 -0
  17. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/python_json_logger.egg-info/requires.txt +0 -0
  18. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/python_json_logger.egg-info/top_level.txt +0 -0
  19. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/__init__.py +0 -0
  20. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/defaults.py +0 -0
  21. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/exception.py +0 -0
  22. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/jsonlogger.py +0 -0
  23. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/py.typed +0 -0
  24. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/src/pythonjsonlogger/utils.py +0 -0
  25. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/tests/__init__.py +0 -0
  26. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/tests/test_deprecation.py +0 -0
  27. {python_json_logger-4.0.0.dev0 → python_json_logger-4.0.0rc1}/tests/test_missing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-json-logger
3
- Version: 4.0.0.dev0
3
+ Version: 4.0.0rc1
4
4
  Summary: JSON Log Formatter for the Python Logging Package
5
5
  Author-email: Zakaria Zajac <zak@madzak.com>, Nicholas Hairs <info+python-json-logger@nicholashairs.com>
6
6
  Maintainer-email: Nicholas Hairs <info+python-json-logger@nicholashairs.com>
@@ -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.0.0.rc1"
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
1
  Metadata-Version: 2.4
2
2
  Name: python-json-logger
3
- Version: 4.0.0.dev0
3
+ Version: 4.0.0rc1
4
4
  Summary: JSON Log Formatter for the Python Logging Package
5
5
  Author-email: Zakaria Zajac <zak@madzak.com>, Nicholas Hairs <info+python-json-logger@nicholashairs.com>
6
6
  Maintainer-email: Nicholas Hairs <info+python-json-logger@nicholashairs.com>
@@ -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,23 +175,40 @@ 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
 
@@ -230,7 +230,6 @@ class BaseJsonFormatter(logging.Formatter):
230
230
  self.reserved_attrs = set(reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS)
231
231
  self.timestamp = timestamp
232
232
 
233
- self._required_fields = self.parse()
234
233
  self._skip_fields = set(self._required_fields)
235
234
  self._skip_fields.update(self.reserved_attrs)
236
235
  self.defaults = defaults if defaults is not None else {}
@@ -269,11 +268,11 @@ class BaseJsonFormatter(logging.Formatter):
269
268
  if record.stack_info and not message_dict.get("stack_info"):
270
269
  message_dict["stack_info"] = self.formatStack(record.stack_info)
271
270
 
272
- log_record: LogRecord = {}
273
- self.add_fields(log_record, record, message_dict)
274
- 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)
275
274
 
276
- return self.serialize_log_record(log_record)
275
+ return self.serialize_log_record(log_data)
277
276
 
278
277
  ## JSON Formatter Specific Methods
279
278
  ## -------------------------------------------------------------------------
@@ -288,6 +287,18 @@ class BaseJsonFormatter(logging.Formatter):
288
287
  Returns:
289
288
  list of fields to be extracted and serialized
290
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
+
291
302
  if isinstance(self._style, logging.StringTemplateStyle):
292
303
  formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX
293
304
 
@@ -302,22 +313,21 @@ class BaseJsonFormatter(logging.Formatter):
302
313
  else:
303
314
  raise ValueError(f"Style {self._style!r} is not supported")
304
315
 
305
- if self._fmt:
306
- return formatter_style_pattern.findall(self._fmt)
307
-
308
- return []
316
+ return formatter_style_pattern.findall(self._fmt)
309
317
 
310
- def serialize_log_record(self, log_record: LogRecord) -> str:
311
- """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
312
320
 
313
321
  Args:
314
- log_record: the log record
322
+ log_data: the data
323
+
324
+ *Changed in 4.0*: `log_record` renamed to `log_data`
315
325
  """
316
- return self.prefix + self.jsonify_log_record(log_record)
326
+ return self.prefix + self.jsonify_log_record(log_data)
317
327
 
318
328
  def add_fields(
319
329
  self,
320
- log_record: Dict[str, Any],
330
+ log_data: Dict[str, Any],
321
331
  record: logging.LogRecord,
322
332
  message_dict: Dict[str, Any],
323
333
  ) -> None:
@@ -326,38 +336,40 @@ class BaseJsonFormatter(logging.Formatter):
326
336
  This method can be overridden to implement custom logic for adding fields.
327
337
 
328
338
  Args:
329
- log_record: data that will be logged
339
+ log_data: data that will be logged
330
340
  record: the record to extract data from
331
341
  message_dict: dictionary that was logged instead of a message. e.g
332
342
  `logger.info({"is_this_message_dict": True})`
343
+
344
+ *Changed in 4.0*: `log_record` renamed to `log_data`
333
345
  """
334
346
  for field in self.defaults:
335
- log_record[self._get_rename(field)] = self.defaults[field]
347
+ log_data[self._get_rename(field)] = self.defaults[field]
336
348
 
337
349
  for field in self._required_fields:
338
- log_record[self._get_rename(field)] = record.__dict__.get(field)
350
+ log_data[self._get_rename(field)] = record.__dict__.get(field)
339
351
 
340
352
  for data_dict in [self.static_fields, message_dict]:
341
353
  for key, value in data_dict.items():
342
- log_record[self._get_rename(key)] = value
354
+ log_data[self._get_rename(key)] = value
343
355
 
344
356
  merge_record_extra(
345
357
  record,
346
- log_record,
358
+ log_data,
347
359
  reserved=self._skip_fields,
348
360
  rename_fields=self.rename_fields,
349
361
  )
350
362
 
351
363
  if self.timestamp:
352
364
  key = self.timestamp if isinstance(self.timestamp, str) else "timestamp"
353
- log_record[self._get_rename(key)] = datetime.fromtimestamp(
365
+ log_data[self._get_rename(key)] = datetime.fromtimestamp(
354
366
  record.created, tz=timezone.utc
355
367
  )
356
368
 
357
369
  if self.rename_fields_keep_missing:
358
370
  for field in self.rename_fields.values():
359
- if field not in log_record:
360
- log_record[field] = None
371
+ if field not in log_data:
372
+ log_data[field] = None
361
373
  return
362
374
 
363
375
  def _get_rename(self, key: str) -> str:
@@ -365,26 +377,30 @@ class BaseJsonFormatter(logging.Formatter):
365
377
 
366
378
  # Child Methods
367
379
  # ..........................................................................
368
- def jsonify_log_record(self, log_record: LogRecord) -> str:
369
- """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.
370
382
 
371
383
  Child classes MUST override this method.
372
384
 
373
385
  Args:
374
- 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`
375
389
  """
376
390
  raise NotImplementedError()
377
391
 
378
- def process_log_record(self, log_record: LogRecord) -> LogRecord:
379
- """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.
380
394
 
381
395
  Child classes can override this method to alter the log record before it
382
396
  is serialized.
383
397
 
384
398
  Args:
385
- log_record: incoming data
399
+ log_data: incoming data
400
+
401
+ *Changed in 4.0*: `log_record` renamed to `log_data`
386
402
  """
387
- return log_record
403
+ return log_data
388
404
 
389
405
  def formatException(self, ei) -> Union[str, list[str]]: # type: ignore
390
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")
@@ -19,12 +19,24 @@ import pytest
19
19
  _LOGGER_COUNT = 0
20
20
  EXT_VAL = 999
21
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
+
22
33
  LOGGING_CONFIG = {
23
34
  "version": 1,
24
35
  "disable_existing_loggers": False,
25
36
  "formatters": {
26
37
  "default": {
27
38
  "()": "pythonjsonlogger.json.JsonFormatter",
39
+ "json_default": "ext://tests.test_dictconfig.my_json_default",
28
40
  "static_fields": {"ext-val": "ext://tests.test_dictconfig.EXT_VAL"},
29
41
  }
30
42
  },
@@ -73,8 +85,12 @@ def env() -> Generator[LoggingEnvironment, None, None]:
73
85
  ### TESTS
74
86
  ### ============================================================================
75
87
  def test_external_reference_support(env: LoggingEnvironment):
76
- env.logger.info("hello")
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()})
77
92
  log_json = env.load_json()
78
93
 
79
94
  assert log_json["ext-val"] == EXT_VAL
95
+ assert log_json["dummy"] == "DUMMY"
80
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")