python-json-logger 3.0.1__py3-none-any.whl → 3.2.0__py3-none-any.whl

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.
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.1
2
+ Name: python-json-logger
3
+ Version: 3.2.0
4
+ Summary: JSON Log Formatter for the Python Logging Package
5
+ Author-email: Zakaria Zajac <zak@madzak.com>, Nicholas Hairs <info+python-json-logger@nicholashairs.com>
6
+ Maintainer-email: Nicholas Hairs <info+python-json-logger@nicholashairs.com>
7
+ License: BSD-2-Clause License
8
+ Project-URL: Homepage, https://nhairs.github.io/python-json-logger
9
+ Project-URL: GitHub, https://github.com/nhairs/python-json-logger
10
+ Classifier: Development Status :: 6 - Mature
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: System :: Logging
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ License-File: NOTICE
27
+ Requires-Dist: typing_extensions; python_version < "3.10"
28
+ Provides-Extra: dev
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"
32
+ Requires-Dist: validate-pyproject[all]; extra == "dev"
33
+ Requires-Dist: black; extra == "dev"
34
+ Requires-Dist: pylint; extra == "dev"
35
+ Requires-Dist: mypy; extra == "dev"
36
+ Requires-Dist: pytest; extra == "dev"
37
+ Requires-Dist: freezegun; extra == "dev"
38
+ Requires-Dist: backports.zoneinfo; python_version < "3.9" and extra == "dev"
39
+ Requires-Dist: tzdata; extra == "dev"
40
+ Requires-Dist: build; extra == "dev"
41
+ Requires-Dist: mkdocs; extra == "dev"
42
+ Requires-Dist: mkdocs-material>=8.5; extra == "dev"
43
+ Requires-Dist: mkdocs-awesome-pages-plugin; extra == "dev"
44
+ Requires-Dist: mdx_truly_sane_lists; extra == "dev"
45
+ Requires-Dist: mkdocstrings[python]; extra == "dev"
46
+ Requires-Dist: mkdocs-gen-files; extra == "dev"
47
+ Requires-Dist: mkdocs-literate-nav; extra == "dev"
48
+ Requires-Dist: mike; extra == "dev"
49
+
50
+ <!-- [![PyPi](https://img.shields.io/pypi/v/python-json-logger.svg)](https://pypi.python.org/pypi/python-json-logger/)
51
+ [![PyPI - Status](https://img.shields.io/pypi/status/python-json-logger)](https://pypi.python.org/pypi/python-json-logger/)
52
+ [![Python Versions](https://img.shields.io/pypi/pyversions/python-json-logger.svg)](https://github.com/nhairs/python-json-logger) -->
53
+ [![License](https://img.shields.io/github/license/nhairs/python-json-logger.svg)](https://github.com/nhairs/python-json-logger)
54
+ ![Build Status](https://github.com/nhairs/python-json-logger/actions/workflows/test-suite.yml/badge.svg)
55
+ #
56
+ # Python JSON Logger
57
+
58
+ Python JSON Logger enables you produce JSON logs when using Python's `logging` package.
59
+
60
+ JSON logs are machine readable allowing for much easier parsing and ingestion into log aggregation tools.
61
+
62
+
63
+ ### 🚨 Important 🚨
64
+
65
+ This repository is a maintained fork of [madzak/python-json-logger](https://github.com/madzak/python-json-logger) pending [a PEP 541 request](https://github.com/pypi/support/issues/3607) for the PyPI package. The future direction of the project is being discussed [here](https://github.com/nhairs/python-json-logger/issues/1).
66
+
67
+ ## Documentation
68
+
69
+ - [Documentation](https://nhairs.github.io/python-json-logger/latest/)
70
+ - [Quickstart Guide](https://nhairs.github.io/python-json-logger/latest/quickstart/)
71
+ - [Change Log](https://nhairs.github.io/python-json-logger/latest/changelog/)
72
+ - [Contributing](https://nhairs.github.io/python-json-logger/latest/contributing/)
73
+
74
+ ## License
75
+
76
+ This project is licensed under the BSD 2 Clause License - see [`LICENSE`](https://github.com/nhairs/python-json-logger/blob/main/LICENSE)
77
+
78
+ ## Authors and Maintainers
79
+
80
+ This project was originally authored by [Zakaria Zajac](https://github.com/madzak) and our wonderful [contributors](https://github.com/nhairs/python-json-logger/graphs/contributors)
81
+
82
+ It is currently maintained by:
83
+
84
+ - [Nicholas Hairs](https://github.com/nhairs) - [nicholashairs.com](https://www.nicholashairs.com)
@@ -0,0 +1,5 @@
1
+ This software includes the following licenced software:
2
+ - mkdocstrings-python
3
+ Copyright (c) 2021, Timothée Mazzucotelli
4
+ Licenced under ISC Licence
5
+ Source: https://github.com/mkdocstrings/python
@@ -0,0 +1,15 @@
1
+ pythonjsonlogger/__init__.py,sha256=mvnVMTGrh32otHxVE7-MsxT9kBvmMyS6I3NCQDe55LY,898
2
+ pythonjsonlogger/core.py,sha256=zrf2vBMPVjPv5ornwWs2TOi68FbWDnuhj_DrcUiuBG0,13328
3
+ pythonjsonlogger/defaults.py,sha256=-XgxIj8ioq7CsnBBMsQhTRpU-lKbLrpQLJTCaT3iH38,6577
4
+ pythonjsonlogger/exception.py,sha256=r3DXDk7TThscnMVNPpVRaRSFHTnY-ygea6nn-FgjwsI,804
5
+ pythonjsonlogger/json.py,sha256=TsKD_1-TDjaeMUg6la_aVzIWd7GBxgAntY6zWzVe7lU,4165
6
+ pythonjsonlogger/msgspec.py,sha256=M5kiIX4RLr1PwvWKx21N8sgk05LCFymBAWM4cRR3VNA,2161
7
+ pythonjsonlogger/orjson.py,sha256=HVhIHo7CrTwj9pZuZzUmj1qsW0coNdVmDKSDycDs-pM,2357
8
+ pythonjsonlogger/py.typed,sha256=4RLptUHQuSqzK6CDbigff8uvWQjwPPQMxikWPkV4OtA,80
9
+ pythonjsonlogger/utils.py,sha256=qr04nb61TkVw7cJTXAtLBfyH_NBvyYUlR_mehH76-RM,1126
10
+ python_json_logger-3.2.0.dist-info/LICENSE,sha256=GOqVF546Xg4k62zhbED7--IxM8JQKTOi91-KPhoFW1Q,1329
11
+ python_json_logger-3.2.0.dist-info/METADATA,sha256=5eZGCDrb3mmrvVyiug9EYBtzaD_PDzZmHw_UUy4dlKc,4357
12
+ python_json_logger-3.2.0.dist-info/NOTICE,sha256=uRQnFcGZCwuIBR2AIHVvhKi9w1KkiI_vAyralfxCsk4,209
13
+ python_json_logger-3.2.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
14
+ python_json_logger-3.2.0.dist-info/top_level.txt,sha256=9G-OsTkbwPgM8t-bXKPmWDBRZv9cbzLIiEDE5EnGk2I,17
15
+ python_json_logger-3.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,29 @@
1
+ ### IMPORTS
2
+ ### ============================================================================
3
+ ## Future
4
+
5
+ ## Standard Library
6
+ import warnings
7
+
8
+ ## Installed
9
+
10
+ ## Application
11
+ import pythonjsonlogger.json
12
+ import pythonjsonlogger.utils
13
+
14
+ ### CONSTANTS
15
+ ### ============================================================================
16
+ ORJSON_AVAILABLE = pythonjsonlogger.utils.package_is_available("orjson")
17
+ MSGSPEC_AVAILABLE = pythonjsonlogger.utils.package_is_available("msgspec")
18
+
19
+
20
+ ### DEPRECATED COMPATIBILITY
21
+ ### ============================================================================
22
+ def __getattr__(name: str):
23
+ if name == "jsonlogger":
24
+ warnings.warn(
25
+ "pythonjsonlogger.jsonlogger has been moved to pythonjsonlogger.json",
26
+ DeprecationWarning,
27
+ )
28
+ return pythonjsonlogger.json
29
+ raise AttributeError(f"module {__name__} has no attribute {name}")
@@ -0,0 +1,370 @@
1
+ """Core functionality shared by all JSON loggers"""
2
+
3
+ ### IMPORTS
4
+ ### ============================================================================
5
+ ## Future
6
+ from __future__ import annotations
7
+
8
+ ## Standard Library
9
+ from datetime import datetime, timezone
10
+ import importlib
11
+ import logging
12
+ import re
13
+ 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
20
+
21
+ ## Installed
22
+
23
+ ## Application
24
+
25
+
26
+ ### CONSTANTS
27
+ ### ============================================================================
28
+ RESERVED_ATTRS: List[str] = [
29
+ "args",
30
+ "asctime",
31
+ "created",
32
+ "exc_info",
33
+ "exc_text",
34
+ "filename",
35
+ "funcName",
36
+ "levelname",
37
+ "levelno",
38
+ "lineno",
39
+ "module",
40
+ "msecs",
41
+ "message",
42
+ "msg",
43
+ "name",
44
+ "pathname",
45
+ "process",
46
+ "processName",
47
+ "relativeCreated",
48
+ "stack_info",
49
+ "thread",
50
+ "threadName",
51
+ ]
52
+ """Default reserved attributes.
53
+
54
+ These come from the [default attributes of `LogRecord` objects](http://docs.python.org/library/logging.html#logrecord-attributes).
55
+
56
+ Note:
57
+ Although considered a constant, this list is dependent on the Python version due to
58
+ different `LogRecord` objects having different attributes in different Python versions.
59
+
60
+ *Changed in 3.0*: `RESERVED_ATTRS` is now `list[str]` instead of `tuple[str, ...]`.
61
+ """
62
+
63
+ if sys.version_info >= (3, 12):
64
+ # taskName added in python 3.12
65
+ RESERVED_ATTRS.append("taskName")
66
+ RESERVED_ATTRS.sort()
67
+
68
+
69
+ STYLE_STRING_TEMPLATE_REGEX = re.compile(r"\$\{(.+?)\}", re.IGNORECASE) # $ style
70
+ STYLE_STRING_FORMAT_REGEX = re.compile(r"\{(.+?)\}", re.IGNORECASE) # { style
71
+ STYLE_PERCENT_REGEX = re.compile(r"%\((.+?)\)", re.IGNORECASE) # % style
72
+
73
+ ## Type Aliases
74
+ ## -----------------------------------------------------------------------------
75
+ OptionalCallableOrStr: TypeAlias = Optional[Union[Callable, str]]
76
+ """Type alias"""
77
+
78
+ LogRecord: TypeAlias = Dict[str, Any]
79
+ """Type alias"""
80
+
81
+
82
+ ### FUNCTIONS
83
+ ### ============================================================================
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
+ def merge_record_extra(
101
+ record: logging.LogRecord,
102
+ target: Dict,
103
+ reserved: Container[str],
104
+ rename_fields: Optional[Dict[str, str]] = None,
105
+ ) -> Dict:
106
+ """
107
+ Merges extra attributes from LogRecord object into target dictionary
108
+
109
+ Args:
110
+ record: logging.LogRecord
111
+ target: dict to update
112
+ reserved: dict or list with reserved keys to skip
113
+ rename_fields: an optional dict, used to rename field names in the output.
114
+ e.g. Rename `levelname` to `log.level`: `{'levelname': 'log.level'}`
115
+
116
+ *Changed in 3.1*: `reserved` is now `Container[str]`.
117
+ """
118
+ if rename_fields is None:
119
+ rename_fields = {}
120
+ for key, value in record.__dict__.items():
121
+ # this allows to have numeric keys
122
+ if key not in reserved and not (hasattr(key, "startswith") and key.startswith("_")):
123
+ target[rename_fields.get(key, key)] = value
124
+ return target
125
+
126
+
127
+ ### CLASSES
128
+ ### ============================================================================
129
+ class BaseJsonFormatter(logging.Formatter):
130
+ """Base class for all formatters
131
+
132
+ Must not be used directly.
133
+
134
+ *New in 3.1*
135
+
136
+ *Changed in 3.2*: `defaults` argument is no longer ignored.
137
+ """
138
+
139
+ _style: Union[logging.PercentStyle, str] # type: ignore[assignment]
140
+
141
+ ## Parent Methods
142
+ ## -------------------------------------------------------------------------
143
+ # pylint: disable=too-many-arguments,super-init-not-called
144
+ def __init__(
145
+ self,
146
+ fmt: Optional[str] = None,
147
+ datefmt: Optional[str] = None,
148
+ style: str = "%",
149
+ validate: bool = True,
150
+ *,
151
+ prefix: str = "",
152
+ rename_fields: Optional[Dict[str, str]] = None,
153
+ rename_fields_keep_missing: bool = False,
154
+ static_fields: Optional[Dict[str, Any]] = None,
155
+ reserved_attrs: Optional[Sequence[str]] = None,
156
+ timestamp: Union[bool, str] = False,
157
+ defaults: Optional[Dict[str, Any]] = None,
158
+ ) -> None:
159
+ """
160
+ Args:
161
+ fmt: string representing fields to log
162
+ datefmt: format to use when formatting `asctime` field
163
+ style: how to extract log fields from `fmt`
164
+ validate: validate `fmt` against style, if implementing a custom `style` you
165
+ must set this to `False`.
166
+ defaults: a dictionary containing default fields that are added before all other fields and
167
+ may be overridden. The supplied fields are still subject to `rename_fields`.
168
+ prefix: an optional string prefix added at the beginning of
169
+ the formatted string
170
+ rename_fields: an optional dict, used to rename field names in the output.
171
+ Rename `message` to `@message`: `{'message': '@message'}`
172
+ rename_fields_keep_missing: When renaming fields, include missing fields in the output.
173
+ static_fields: an optional dict, used to add fields with static values to all logs
174
+ reserved_attrs: an optional list of fields that will be skipped when
175
+ outputting json log record. Defaults to [all log record attributes][pythonjsonlogger.core.RESERVED_ATTRS].
176
+ timestamp: an optional string/boolean field to add a timestamp when
177
+ outputting the json log record. If string is passed, timestamp will be added
178
+ to log record using string as key. If True boolean is passed, timestamp key
179
+ will be "timestamp". Defaults to False/off.
180
+
181
+ *Changed in 3.1*:
182
+
183
+ - you can now use custom values for style by setting validate to `False`.
184
+ The value is stored in `self._style` as a string. The `parse` method will need to be
185
+ overridden in order to support the new style.
186
+ - Renaming fields now preserves the order that fields were added in and avoids adding
187
+ missing fields. The original behaviour, missing fields have a value of `None`, is still
188
+ available by setting `rename_fields_keep_missing` to `True`.
189
+ """
190
+ ## logging.Formatter compatibility
191
+ ## ---------------------------------------------------------------------
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())}")
206
+
207
+ self.datefmt = datefmt
208
+
209
+ ## JSON Logging specific
210
+ ## ---------------------------------------------------------------------
211
+ self.prefix = prefix
212
+ self.rename_fields = rename_fields if rename_fields is not None else {}
213
+ self.rename_fields_keep_missing = rename_fields_keep_missing
214
+ self.static_fields = static_fields if static_fields is not None else {}
215
+ self.reserved_attrs = set(reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS)
216
+ self.timestamp = timestamp
217
+
218
+ self._required_fields = self.parse()
219
+ self._skip_fields = set(self._required_fields)
220
+ self._skip_fields.update(self.reserved_attrs)
221
+ self.defaults = defaults if defaults is not None else {}
222
+ return
223
+
224
+ def format(self, record: logging.LogRecord) -> str:
225
+ """Formats a log record and serializes to json
226
+
227
+ Args:
228
+ record: the record to format
229
+ """
230
+ message_dict: Dict[str, Any] = {}
231
+ # TODO: logging.LogRecord.msg and logging.LogRecord.message in typeshed
232
+ # are always type of str. We shouldn't need to override that.
233
+ if isinstance(record.msg, dict):
234
+ message_dict = record.msg
235
+ record.message = ""
236
+ else:
237
+ record.message = record.getMessage()
238
+
239
+ # only format time if needed
240
+ if "asctime" in self._required_fields:
241
+ record.asctime = self.formatTime(record, self.datefmt)
242
+
243
+ # Display formatted exception, but allow overriding it in the
244
+ # user-supplied dict.
245
+ if record.exc_info and not message_dict.get("exc_info"):
246
+ message_dict["exc_info"] = self.formatException(record.exc_info)
247
+ if not message_dict.get("exc_info") and record.exc_text:
248
+ message_dict["exc_info"] = record.exc_text
249
+
250
+ # Display formatted record of stack frames
251
+ # default format is a string returned from :func:`traceback.print_stack`
252
+ if record.stack_info and not message_dict.get("stack_info"):
253
+ message_dict["stack_info"] = self.formatStack(record.stack_info)
254
+
255
+ log_record: LogRecord = {}
256
+ self.add_fields(log_record, record, message_dict)
257
+ log_record = self.process_log_record(log_record)
258
+
259
+ return self.serialize_log_record(log_record)
260
+
261
+ ## JSON Formatter Specific Methods
262
+ ## -------------------------------------------------------------------------
263
+ def parse(self) -> List[str]:
264
+ """Parses format string looking for substitutions
265
+
266
+ This method is responsible for returning a list of fields (as strings)
267
+ to include in all log messages.
268
+
269
+ You can support custom styles by overriding this method.
270
+
271
+ Returns:
272
+ list of fields to be extracted and serialized
273
+ """
274
+ if isinstance(self._style, logging.StringTemplateStyle):
275
+ formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX
276
+
277
+ elif isinstance(self._style, logging.StrFormatStyle):
278
+ formatter_style_pattern = STYLE_STRING_FORMAT_REGEX
279
+
280
+ elif isinstance(self._style, logging.PercentStyle):
281
+ # PercentStyle is parent class of StringTemplateStyle and StrFormatStyle
282
+ # so it must be checked last.
283
+ formatter_style_pattern = STYLE_PERCENT_REGEX
284
+
285
+ else:
286
+ raise ValueError(f"Style {self._style!r} is not supported")
287
+
288
+ if self._fmt:
289
+ return formatter_style_pattern.findall(self._fmt)
290
+
291
+ return []
292
+
293
+ def serialize_log_record(self, log_record: LogRecord) -> str:
294
+ """Returns the final representation of the log record.
295
+
296
+ Args:
297
+ log_record: the log record
298
+ """
299
+ return self.prefix + self.jsonify_log_record(log_record)
300
+
301
+ def add_fields(
302
+ self,
303
+ log_record: Dict[str, Any],
304
+ record: logging.LogRecord,
305
+ message_dict: Dict[str, Any],
306
+ ) -> None:
307
+ """Extract fields from a LogRecord for logging
308
+
309
+ This method can be overridden to implement custom logic for adding fields.
310
+
311
+ Args:
312
+ log_record: data that will be logged
313
+ record: the record to extract data from
314
+ message_dict: dictionary that was logged instead of a message. e.g
315
+ `logger.info({"is_this_message_dict": True})`
316
+ """
317
+ for field in self.defaults:
318
+ log_record[self._get_rename(field)] = self.defaults[field]
319
+
320
+ for field in self._required_fields:
321
+ log_record[self._get_rename(field)] = record.__dict__.get(field)
322
+
323
+ for data_dict in [self.static_fields, message_dict]:
324
+ for key, value in data_dict.items():
325
+ log_record[self._get_rename(key)] = value
326
+
327
+ merge_record_extra(
328
+ record,
329
+ log_record,
330
+ reserved=self._skip_fields,
331
+ rename_fields=self.rename_fields,
332
+ )
333
+
334
+ if self.timestamp:
335
+ key = self.timestamp if isinstance(self.timestamp, str) else "timestamp"
336
+ log_record[self._get_rename(key)] = datetime.fromtimestamp(
337
+ record.created, tz=timezone.utc
338
+ )
339
+
340
+ if self.rename_fields_keep_missing:
341
+ for field in self.rename_fields.values():
342
+ if field not in log_record:
343
+ log_record[field] = None
344
+ return
345
+
346
+ def _get_rename(self, key: str) -> str:
347
+ return self.rename_fields.get(key, key)
348
+
349
+ # Child Methods
350
+ # ..........................................................................
351
+ def jsonify_log_record(self, log_record: LogRecord) -> str:
352
+ """Convert this log record into a JSON string.
353
+
354
+ Child classes MUST override this method.
355
+
356
+ Args:
357
+ log_record: the data to serialize
358
+ """
359
+ raise NotImplementedError()
360
+
361
+ def process_log_record(self, log_record: LogRecord) -> LogRecord:
362
+ """Custom processing of the log record.
363
+
364
+ Child classes can override this method to alter the log record before it
365
+ is serialized.
366
+
367
+ Args:
368
+ log_record: incoming data
369
+ """
370
+ return log_record