Based on the requirements and use cases, identify the main objects of the system and analyze how they interact and relate to each other...
A developer writes logger.info("User logged in", "AuthService"). What happens? The Logger checks whether INFO meets the current threshold. If it does, it creates a LogEntry, an immutable record capturing the timestamp, level, message, source class, and thread name right now, at the moment of the call. The Logger then iterates through its registered handlers. A ConsoleHandler prints the entry to stdout. A FileHandler writes it to a rotating log file. Each handler has its own level threshold: the console might show everything from DEBUG up, while the file records only ERROR and above. The handlers are all LogHandler subclasses. The Logger itself is a Singleton. One shared instance ensures all application classes use the same configuration. The LogLevel enum (DEBUG through FATAL) provides type-safe severity comparison. Not every noun becomes a class. "Threshold" is a field on Logger rather than its own entity, but nouns still give you a strong starting lineup.
The logger needs to create a LogEntry: an immutable record carrying the timestamp, log level, message, source class, and thread name. LogEntry is immutable because once created, a log record should never change. Modifying a log entry after creation would undermine the audit trail.
The logger itself is the Logger: a singleton that receives log calls, filters by level, and dispatches to handlers. Singleton because all application classes should share the same logger configuration and handler list.
Why Singleton? If each class created its own Logger, configuration would be scattered. Changing the log level would require updating every instance. A single shared Logger ensures one configuration point.
A LogLevel enum (DEBUG, INFO, WARN, ERROR, FATAL) with numeric priorities enables threshold comparison. DEBUG(0) is the lowest priority; FATAL(4) is the highest. A message is logged only if its level priority is at or above the threshold.
Each output destination is a LogHandler: an abstract class with a handle(LogEntry) method. Concrete handlers include ConsoleHandler (writes to stdout/stderr) and FileHandler (writes to a file with rotation). Handlers can have their own level thresholds for fine-grained control.
For each class, define the attributes (data) it will hold and the methods (functions) that operate on the attributes. Ensure they align with the object's responsibilities and adhere to the principle of encapsulation. Write your code in the code editor below.
☕Java🐍PythonJavaScriptTypeScriptC++
LogEntry
from __future__ import annotations
from datetime import datetime
from enum import Enum
import threading
class LogLevel(Enum):
DEBUG = 'DEBUG'
INFO = 'INFO'
WARNING = 'WARNING'
ERROR = 'ERROR'
CRITICAL = 'CRITICAL'
class LogEntry:
FORMATTER = "%Y-%m-%d %H:%M:%S.%f"
def __init__(self, level: LogLevel, message: str, source_class: str):
self._timestamp = datetime.now()
self._level = level
self._message = message
self._source_class = source_class
self._thread_name = threading.current_thread().name
def format(self) -> str:
return f"{self._timestamp.strftime(self.FORMATTER)} [{self._level.name}] [{self._thread_name}] {self._source_class} - {self._message}"
@property
def timestamp(self) -> datetime:
return self._timestamp
@property
def level(self) -> LogLevel:
return self._level
@property
def message(self) -> str:
return self._message
@property
def source_class(self) -> str:
return self._source_class
@property
def thread_name(self) -> str:
return self._thread_name
All fields are final. The timestamp and thread name are captured at construction time, ensuring they reflect the moment the log call was made, not when the entry is eventually written (important for asynchronous logging). With the data record defined, the natural question is: where does it go?
☕Java🐍PythonJavaScriptTypeScriptC++
ConsoleHandler
from future import annotations
from threading import RLock
class ConsoleHandler(LogHandler):
def init(self, handler_level: LogLevel):
super().init(handler_level)
def do_handle(self, entry: LogEntry) -> None:
if entry.level.is_at_least(LogLevel.ERROR):
print(entry.format(), file=sys.stderr)
else:
print(entry.format())
sys.stdout.flush()
ConsoleHandler is the simplest destination: it routes ERROR and FATAL to stderr and everything else to stdout. The next handler adds file-based persistence with rotation.
☕Java🐍PythonJavaScriptTypeScriptC++
FileHandler
from future import annotations
from threading import Lock
from datetime import datetime
import os
class FileHandler(LogHandler):
def init(self, handler_level: LogLevel, file_path: str, max_file_size: int):
super().init(handler_level)
self.file_path = file_path
self.max_file_size = max_file_size
self.current_size = 0
self.write_lock = Lock()
try:
self.writer = open(file_path, 'a')
if os.path.exists(file_path):
self.current_size = os.path.getsize(file_path)
except IOError as e:
raise RuntimeError(f"Failed to open log file: {file_path}") from e
def do_handle(self, entry: LogEntry):
formatted = entry.format() + '\n'
with self.write_lock:
try:
if self.current_size + len(formatted) > self.max_file_size:
self.rotate()
self.writer.write(formatted)
self.writer.flush()
self.current_size += len(formatted)
except IOError as e:
print(f"Failed to write log: {e}", file=sys.stderr)
def rotate(self):
self.writer.close()
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
os.rename(self.file_path, f"{self.file_path}.{ts}")
self.writer = open(self.file_path, 'w')
self.current_size = 0
FileHandler uses its own writeLock to synchronize file writes and rotation. This provides defense-in-depth: even if the Logger-level lock is relaxed for async logging, individual handlers remain thread-safe. Finally, the Logger singleton ties the system together.
☕Java🐍PythonJavaScriptTypeScriptC++
Logger
from future import annotations
from threading import RLock
from typing import List
class Logger:
_instance: Logger = None
_instance_lock = RLock()
def __init__(self) -> None:
self._handlers: List[LogHandler] = []
self._log_level: LogLevel = LogLevel.INFO
self._log_lock = RLock()
@classmethod
def get_instance(cls) -> Logger:
if cls._instance is None:
with cls._instance_lock:
if cls._instance is None:
cls._instance = Logger()
return cls._instance
def log(self, level: LogLevel, message: str, source_class: str) -> None:
if not level.is_at_least(self._log_level):
return
entry = LogEntry(level, message, source_class)
with self._log_lock:
for handler in self._handlers:
handler.handle(entry)
def add_handler(self, handler: LogHandler) -> None:
self._handlers.append(handler)
def set_log_level(self, level: LogLevel) -> None:
self._log_level = level
The logLevel field is volatile so changes by one thread are immediately visible to all other threads without requiring a lock.
Explain design tradeoffs you considered. Check and explain whether your design adheres to SOLID principles. Explain how your design can handle changes in scale and whether it would be easy to extend with new functionalities. Identify areas for future improvement...
Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle