A structured logger that outputs JSON format logs with consistent fields.
| 24 | |
| 25 | |
| 26 | class StructuredLogger: |
| 27 | """A structured logger that outputs JSON format logs with consistent fields.""" |
| 28 | |
| 29 | def __init__(self, name: str, log_level: LogLevel = LogLevel.INFO, log_file: str = None): |
| 30 | self.name = name |
| 31 | self.log_level = log_level |
| 32 | self.logger = logging.getLogger(name) |
| 33 | self.logger.setLevel(self._get_logging_level(log_level)) |
| 34 | |
| 35 | # Create formatter |
| 36 | formatter = logging.Formatter('%(message)s') |
| 37 | |
| 38 | # Create handler |
| 39 | if log_file: |
| 40 | # Ensure log directory exists |
| 41 | log_path = Path(log_file) |
| 42 | log_path.parent.mkdir(parents=True, exist_ok=True) |
| 43 | handler = logging.FileHandler(log_file) |
| 44 | else: |
| 45 | handler = logging.StreamHandler(sys.stdout) |
| 46 | |
| 47 | handler.setFormatter(formatter) |
| 48 | self.logger.addHandler(handler) |
| 49 | |
| 50 | # For correlation IDs |
| 51 | self.correlation_id = None |
| 52 | |
| 53 | def _get_logging_level(self, log_level: LogLevel) -> int: |
| 54 | """Convert LogLevel enum to logging module level.""" |
| 55 | level_map = { |
| 56 | LogLevel.DEBUG: logging.DEBUG, |
| 57 | LogLevel.INFO: logging.INFO, |
| 58 | LogLevel.WARNING: logging.WARNING, |
| 59 | LogLevel.ERROR: logging.ERROR, |
| 60 | LogLevel.CRITICAL: logging.CRITICAL |
| 61 | } |
| 62 | return level_map.get(log_level, logging.INFO) |
| 63 | |
| 64 | def _should_log(self, level: LogLevel) -> bool: |
| 65 | """Check if a log level should be logged based on configured level.""" |
| 66 | return level >= self.log_level |
| 67 | |
| 68 | def _format_log(self, log_type: LogType, level: LogLevel, message: str, |
| 69 | correlation_id: str = None, **kwargs) -> str: |
| 70 | """Format log entry as JSON string.""" |
| 71 | log_entry = { |
| 72 | "timestamp": datetime.datetime.now(datetime.UTC), |
| 73 | "log_type": log_type.value, |
| 74 | "level": level.value, |
| 75 | "logger": self.name, |
| 76 | "message": message, |
| 77 | "correlation_id": correlation_id or self.correlation_id, |
| 78 | **kwargs |
| 79 | } |
| 80 | return json.dumps(log_entry, default=str) |
| 81 | |
| 82 | def _log(self, log_type: LogType, level: LogLevel, message: str, |
| 83 | correlation_id: str = None, **kwargs): |
no outgoing calls
no test coverage detected