|
| 1 | +import json |
| 2 | +import logging |
| 3 | +from typing import Any |
| 4 | + |
| 5 | + |
| 6 | +def json_formatter(unserialized_value: Any): |
| 7 | + """JSON custom serializer to cast unserialisable values to strings. |
| 8 | +
|
| 9 | + Example |
| 10 | + ------- |
| 11 | +
|
| 12 | + **Serialize unserialisable value to string** |
| 13 | +
|
| 14 | + class X: pass |
| 15 | + value = {"x": X()} |
| 16 | +
|
| 17 | + json.dumps(value, default=json_formatter) |
| 18 | +
|
| 19 | + Parameters |
| 20 | + ---------- |
| 21 | + unserialized_value: Any |
| 22 | + Python object unserializable by JSON |
| 23 | + """ |
| 24 | + return str(unserialized_value) |
| 25 | + |
| 26 | + |
| 27 | +class JsonFormatter(logging.Formatter): |
| 28 | + """AWS Lambda Logging formatter. |
| 29 | +
|
| 30 | + Formats the log message as a JSON encoded string. If the message is a |
| 31 | + dict it will be used directly. If the message can be parsed as JSON, then |
| 32 | + the parse d value is used in the output record. |
| 33 | +
|
| 34 | + Originally taken from https://gitlab.com/hadrien/aws_lambda_logging/ |
| 35 | +
|
| 36 | + """ |
| 37 | + |
| 38 | + def __init__(self, **kwargs): |
| 39 | + """Return a JsonFormatter instance. |
| 40 | +
|
| 41 | + The `json_default` kwarg is used to specify a formatter for otherwise |
| 42 | + unserialisable values. It must not throw. Defaults to a function that |
| 43 | + coerces the value to a string. |
| 44 | +
|
| 45 | + Other kwargs are used to specify log field format strings. |
| 46 | + """ |
| 47 | + datefmt = kwargs.pop("datefmt", None) |
| 48 | + |
| 49 | + super(JsonFormatter, self).__init__(datefmt=datefmt) |
| 50 | + self.reserved_keys = ["timestamp", "level", "location"] |
| 51 | + self.format_dict = { |
| 52 | + "timestamp": "%(asctime)s", |
| 53 | + "level": "%(levelname)s", |
| 54 | + "location": "%(funcName)s:%(lineno)d", |
| 55 | + } |
| 56 | + self.format_dict.update(kwargs) |
| 57 | + self.default_json_formatter = kwargs.pop("json_default", json_formatter) |
| 58 | + |
| 59 | + def format(self, record): # noqa: A003 |
| 60 | + record_dict = record.__dict__.copy() |
| 61 | + record_dict["asctime"] = self.formatTime(record, self.datefmt) |
| 62 | + |
| 63 | + log_dict = {} |
| 64 | + for key, value in self.format_dict.items(): |
| 65 | + if value and key in self.reserved_keys: |
| 66 | + # converts default logging expr to its record value |
| 67 | + # e.g. '%(asctime)s' to '2020-04-24 09:35:40,698' |
| 68 | + log_dict[key] = value % record_dict |
| 69 | + else: |
| 70 | + log_dict[key] = value |
| 71 | + |
| 72 | + if isinstance(record_dict["msg"], dict): |
| 73 | + log_dict["message"] = record_dict["msg"] |
| 74 | + else: |
| 75 | + log_dict["message"] = record.getMessage() |
| 76 | + |
| 77 | + # Attempt to decode the message as JSON, if so, merge it with the |
| 78 | + # overall message for clarity. |
| 79 | + try: |
| 80 | + log_dict["message"] = json.loads(log_dict["message"]) |
| 81 | + except (json.decoder.JSONDecodeError, TypeError, ValueError): |
| 82 | + pass |
| 83 | + |
| 84 | + if record.exc_info: |
| 85 | + # Cache the traceback text to avoid converting it multiple times |
| 86 | + # (it's constant anyway) |
| 87 | + # from logging.Formatter:format |
| 88 | + if not record.exc_text: |
| 89 | + record.exc_text = self.formatException(record.exc_info) |
| 90 | + |
| 91 | + if record.exc_text: |
| 92 | + log_dict["exception"] = record.exc_text |
| 93 | + |
| 94 | + json_record = json.dumps(log_dict, default=self.default_json_formatter) |
| 95 | + |
| 96 | + if hasattr(json_record, "decode"): # pragma: no cover |
| 97 | + json_record = json_record.decode("utf-8") |
| 98 | + |
| 99 | + return json_record |
0 commit comments