Plain is headed towards 1.0! Subscribe for development updates →

 1import json
 2import logging
 3
 4
 5class KeyValueFormatter(logging.Formatter):
 6    """Formatter that outputs key-value pairs from Plain's context system."""
 7
 8    def format(self, record):
 9        # Build key-value pairs from context
10        kv_pairs = []
11
12        # Look for Plain's context data
13        if hasattr(record, "context") and isinstance(record.context, dict):
14            for key, value in record.context.items():
15                formatted_value = self._format_value(value)
16                kv_pairs.append(f"{key}={formatted_value}")
17
18        # Add the keyvalue attribute to the record for %(keyvalue)s substitution
19        record.keyvalue = " ".join(kv_pairs)
20
21        # Let the parent formatter handle the format string with %(keyvalue)s
22        return super().format(record)
23
24    @staticmethod
25    def _format_value(value):
26        """Format a value for key-value output."""
27        if isinstance(value, str):
28            s = value
29        else:
30            s = str(value)
31
32        if '"' in s:
33            # Escape quotes and surround it
34            s = s.replace('"', '\\"')
35            s = f'"{s}"'
36        elif s == "":
37            # Quote empty strings instead of printing nothing
38            s = '""'
39        elif any(char in s for char in [" ", "/", "'", ":", "=", "."]):
40            # Surround these with quotes for parsers
41            s = f'"{s}"'
42
43        return s
44
45
46class JSONFormatter(logging.Formatter):
47    """Formatter that outputs JSON from Plain's context system, with optional format string."""
48
49    def format(self, record):
50        # Build the JSON object from Plain's context data
51        log_obj = {
52            "timestamp": self.formatTime(record),
53            "level": record.levelname,
54            "message": record.getMessage(),
55            "logger": record.name,
56        }
57
58        # Add Plain's context data to the main JSON object
59        if hasattr(record, "context") and isinstance(record.context, dict):
60            log_obj.update(record.context)
61
62        # Handle exceptions
63        if record.exc_info:
64            log_obj["exception"] = self.formatException(record.exc_info)
65
66        # Add the json attribute to the record for %(json)s substitution
67        record.json = json.dumps(log_obj, default=str, ensure_ascii=False)
68
69        # Let the parent formatter handle the format string with %(json)s
70        return super().format(record)