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)