Coverage for src/setlogging/logger.py: 82%
103 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-22 02:34 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-22 02:34 +0000
1# Standard library imports
2from datetime import datetime, timezone as dt_timezone
3import json
4import logging
5from logging.handlers import RotatingFileHandler
6import os
7from typing import Optional, Union
9TIMEZONE=datetime.now().astimezone().tzinfo
11# class TimezoneFormatter(logging.Formatter):
12# """
13# Custom formatter to include timezone-aware timestamps in log messages.
15# Args:
16# fmt: The format string for the log message
17# datefmt: The format string for the timestamp
18# timezone: Optional specific timezone to use (defaults to local)
20# Example:
21# formatter = TimezoneFormatter(
22# fmt='%(asctime)s [%(timezone)s] %(levelname)s: %(message)s',
23# datefmt='%Y-%m-%d %H:%M:%S'
24# )
25# """
27# def __init__(
28# self,
29# fmt: Optional[str] = None,
30# datefmt: Optional[str] = None
31# ) -> None:
32# super().__init__(fmt, datefmt)
33# self.local_timezone = TIMEZONE
35# def formatTime(self, record: logging.LogRecord, datefmt: Optional[str] = None) -> str:
36# try:
37# local_dt = datetime.fromtimestamp(
38# record.created, self.local_timezone)
39# if datefmt:
40# return local_dt.strftime(datefmt)
41# else:
42# return local_dt.isoformat()
43# except Exception as e:
44# raise RuntimeError(f"Failed to format time: {str(e)}") from e
46# def format(self, record: logging.LogRecord) -> str:
47# # Add timezone name to log record
48# record.timezone = str(self.local_timezone)
49# return super().format(record)
52class CustomFormatter(logging.Formatter):
53 def formatTime(self, record, datefmt):
54 try:
55 # Ensure datefmt is not None to avoid string concatenation errors
56 if datefmt is None:
57 datefmt = "%Y-%m-%d %H:%M:%S" # Default time format
59 # Create a timezone-aware datetime object
60 tz_aware_time = datetime.fromtimestamp(record.created, tz=TIMEZONE)
62 # Format the time with milliseconds
63 formatted_time_with_ms = tz_aware_time.strftime(datefmt + ".%f")
64 formatted_time = formatted_time_with_ms[:-3] # Truncate to 3 decimal places for milliseconds
66 # Get the timezone abbreviation (e.g., EST)
67 timezone_abbr = tz_aware_time.strftime("%Z")
69 # Combine the formatted time, milliseconds, and timezone
70 return f"{formatted_time} {timezone_abbr}"
72 except Exception as e:
73 # Fallback to the parent class's default time formatting in case of errors
74 return super().formatTime(record, datefmt)
77def setup_logging(
78 log_level: int = logging.DEBUG,
79 log_file: Optional[str] = None,
80 max_size_mb: int = 25, # 25MB
81 backup_count: int = 7,
82 console_output: bool = True,
83 log_format: Optional[str] = None,
84 date_format: Optional[str] = None,
85 json_format: bool = False,
86 indent: Optional[int] = None
87) -> logging.Logger:
88 """
89 Configure logging system with rotating file handler and optional console output.
91 Args:
92 log_level: Logging level (default: DEBUG)
93 log_file: Log file path (default: app.log or app_json.log if json_format is True)
94 max_size_mb: Max log file size in MB before rotation (default: 25MB)
95 backup_count: Number of backup files to keep (default: 7)
96 console_output: Enable console logging (default: True)
97 log_format: Custom log format string (optional)
98 date_format: Custom date format string (optional)
99 json_format: Flag to determine if log format should be JSON (default: False)
100 indent: Indentation level for JSON output (default: None)
101 """
102 try:
103 if max_size_mb <= 0:
104 raise ValueError("max_size_mb must be positive")
105 if backup_count < 0:
106 raise ValueError("backup_count must be non-negative")
107 if indent is not None:
108 if indent < 0:
109 raise ValueError("indent must be non-negative")
110 if not json_format:
111 raise ValueError("indent parameter is only valid when json_format is True")
113 # Validate log level
114 valid_levels = {
115 logging.DEBUG, logging.INFO, logging.WARNING,
116 logging.ERROR, logging.CRITICAL
117 }
118 if log_level not in valid_levels:
119 raise ValueError(f"Invalid log level: {log_level}. Valid levels are: {valid_levels}")
121 # Validate the date_format
122 if date_format:
123 valid_codes = {"%Y", "%m", "%d", "%H", "%M", "%S", "%z", "%Z"}
124 if not any(code in date_format for code in valid_codes):
125 raise ValueError(f"Invalid date_format: {date_format} must contain at least one format code (e.g., %Y, %m, %H)")
127 # Validate the log_format
128 if log_format:
129 valid_codes = {"%(asctime)s", "%(levelname)s", "%(name)s", "%(message)s"}
130 if not any(code in log_format for code in valid_codes):
131 raise ValueError(f"Invalid log_format: {log_format} must contain at least one format code (e.g., %(asctime)s, %(levelname)s)")
133 # Calculate max file size in bytes
134 max_bytes = max_size_mb * 1024 * 1024
136 # Set default log file if not provided
137 log_file = log_file or ("app_json.log" if json_format else "app.log")
139 # Create log directory if it does not exist
140 log_dir = os.path.dirname(log_file)
141 if log_dir: # If log_dir is not empty
142 os.makedirs(log_dir, exist_ok=True) # Create directory if it does not exist
144 # check if the directory is writable
145 test_file = os.path.join(log_dir, ".permission_test")
146 try:
147 with open(test_file, "w") as f:
148 f.write("test")
149 os.remove(test_file)
150 except IOError as e:
151 raise PermissionError(f"Directory not writable: {log_dir}") from e
153 # Check if log file is writable
154 if os.path.exists(log_file):
155 if not os.access(log_file, os.W_OK):
156 raise PermissionError(f"File not writable: {log_file}")
159 except Exception as e: # Catch permission errors
160 raise
163 try:
164 # Create logger
165 logger = logging.getLogger(__name__)
166 logger.setLevel(log_level)
168 # Clear existing handlers
169 logger.handlers = []
171 # Set up formatter
172 if json_format:
173 formatter = logging.Formatter(json.dumps({
174 "time": "%(asctime)s",
175 "name": "%(name)s",
176 "level": "%(levelname)s",
177 "message": "%(message)s"
178 }, indent=indent))
179 else:
180 formatter = CustomFormatter(
181 log_format or "%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
182 date_format or "%Y-%m-%d %H:%M:%S"
183 )
185 # Set up file handler
186 file_handler = RotatingFileHandler(
187 log_file, maxBytes=max_bytes, backupCount=backup_count)
188 file_handler.setFormatter(formatter)
189 logger.addHandler(file_handler)
191 # Set up console handler if enabled
192 if console_output:
193 console_handler = logging.StreamHandler()
194 console_handler.setFormatter(formatter)
195 logger.addHandler(console_handler)
197 # Generate configuration details using get_config_message
198 config_message = get_config_message(
199 log_level=log_level,
200 file_handler=file_handler,
201 max_size_mb=max_size_mb,
202 backup_count=backup_count,
203 console_output=console_output,
204 json_format=json_format, # Adapt the format based on user preference
205 indent=indent
206 )
208 # Log configuration details with respect to log_level
209 if json_format:
210 # Parse JSON as dictionary
211 config_dict = json.loads(config_message)
212 if log_level != 0:
213 logger.log(log_level, {"Logging Configuration": config_dict})
214 else:
215 logger.warning({"Logging Configuration": config_dict})
216 else:
217 if log_level != 0:
218 logger.log(log_level, (
219 f"Logging Configuration:\n"
220 f"{config_message}"
221 ))
222 else:
223 logger.warning(f"Logging Configuration:\n{config_message}")
225 return logger
227 except Exception as e:
228 raise RuntimeError(f"Failed to set up logging: {str(e)}") from e
231def get_config_message(log_level, file_handler, max_size_mb, backup_count, console_output, json_format=False, indent=None):
232 processID = os.getpid()
234 if json_format:
235 config_dict = {
236 "Level": logging.getLevelName(log_level),
237 "LogFile": file_handler.baseFilename,
238 "MaxFileSizeMB": max_size_mb,
239 "BackupCount": backup_count,
240 "ConsoleOutput": console_output,
241 "Timezone": str(TIMEZONE),
242 "ProcessID": processID
243 }
244 return json.dumps(config_dict)
245 else:
246 return f"""
247===============================
248 Logging Configuration
249===============================
250Level : {logging.getLevelName(log_level)}
251Log File : {file_handler.baseFilename}
252Max File Size: {max_size_mb:.2f} MB
253Backup Count : {backup_count}
254Console Out : {console_output}
255Timezone : {TIMEZONE}
256ProcessID : {processID}
257===============================
258"""
261def get_logger(
262 name: str = __name__,
263 log_level: int = logging.DEBUG,
264 log_file: Optional[str] = None,
265 max_size_mb: int = 25, # 25MB
266 backup_count: int = 7,
267 console_output: bool = True,
268 log_format: Optional[str] = None,
269 date_format: Optional[str] = None,
270 json_format: bool = False,
271 indent: Optional[int] = None
272) -> logging.Logger:
273 """
274 Simplified function to set up logging and return a logger instance.
276 Args:
277 name: Name of the logger.
278 log_level: Logging level.
279 log_file: Log file name.
280 max_size_mb: Max size of log file in MB before rotation.
281 backup_count: Number of rotated backups to keep.
282 console_output: Enable console logging (default: True)
283 log_format: Custom log format string (optional)
284 date_format: Custom date format string (optional)
285 json_format: Flag to determine if log format should be JSON.
286 indent: Indentation level for JSON output.
288 Returns:
289 logging.Logger: Configured logger instance.
290 """
291 return setup_logging(
292 log_level=log_level,
293 log_file=log_file,
294 max_size_mb=max_size_mb, # Pass max_size_mb parameter
295 backup_count=backup_count,
296 console_output=console_output,
297 log_format=log_format,
298 date_format=date_format,
299 json_format=json_format,
300 indent=indent
301 )
304# Example Usage
305if __name__ == "__main__":
306 try:
307 logger = get_logger(console_output=True)
308 logger.debug("Basic debug example")
309 logger.info("Basic usage example")
310 logger.info(datetime.now().astimezone().tzinfo)
311 # JSON format example
312 json_logger = get_logger(json_format=True, indent=2)
313 json_logger.info("JSON format example")
316 except Exception as e:
317 print(f"Error: {str(e)}")
318 raise