I am trying to become better at designing classes for dependency injection and IOC in general. It is often not an intuitive concept.
I have the following python interface and implementation for a string-based logger.
class ILogger(ABC):
@abstractmethod
def info(self, msg: str):
pass
@abstractmethod
def debug(self, msg: str):
pass
@abstractmethod
def error(self, msg: str):
pass
@abstractmethod
def warning(self, msg: str):
pass
class StandardLogger(ILogger):
def __init__(self, logger: Logger) -> None:
self._logger = logger
def info(self, msg: str):
self._logger.info(msg)
def debug(self, msg: str):
self._logger.debug(msg)
def error(self, msg: str):
self._logger.error(msg)
def warning(self, msg: str):
self._logger.warning(msg)
This has been very successful in conjunction with the composite pattern as I have had various logging requirements from management that need to be used simultaneously and configurable at runtime. EG Windows event logging, file logging, logging to a file over ssh, moving to LOGORU instead. But, the problem I am facing is application events cannot provide enough information to this kind of logger, EG we need to provide ids to events.
So I would like to segregate the idea of a developer log (any logs which would be useful for debugging the system) from an event log, which is more useful for administrators of the system to tell what is occurring.
This mainly stems from a requirement to log to the Windows event log with unique event ids for each event type and differing information.
My first thought is another interface that is very specific to the application;
class EventType(IntEnum):
"""Represents all possible events in the application.
Might be better to simply add an id field to the AbstractEvent class for the sake of DRY
"""
RESOURCE_AVAILABLE = 1
RESOURCE_TRANSFER_STARTED = 2
RESOURCE_TRANSFER_FINISHED = 3
RESOURCE_VALIDATED = 4
RESOURCE_INVALIDATED = 5
RESOURCE_MISSING = 6
RESOURCE_INCORRECT = 7
RESOURCE_TRANSFER_SUCCESSFUL = 8
RESOURCE_TRANSFER_PARTIAL = 9
RESOURCE_TRANSFER_FAILED = 10
FILESYSTEM_EVENT_RECEIVED = 11
@dataclass(frozen=True)
class AbstractEvent(ABC):
type: EventType = field(init=False)
utc_timestamp: datetime = field(default=datetime.utcnow(), init=False)
class IEventLogger(ABC):
@abstractmethod
def log(self, event: AbstractEvent):
pass
@dataclass(frozen=True)
class ResourceEvent(AbstractEvent):
resource_path: str
resource_type: FileSystemResourceType
size: int
class ResourceAvailable(ResourceEvent):
type = EventType.RESOURCE_AVAILABLE
class ResourceTransferStarted(ResourceEvent):
type = EventType.RESOURCE_TRANSFER_STARTED
class ResourceTransferFinished(ResourceEvent):
type = EventType.RESOURCE_TRANSFER_FINISHED
class ResourceValidated(ResourceEvent):
type = EventType.RESOURCE_VALIDATED
@dataclass(frozen=True)
class ResourceInvalidated(ResourceEvent):
type = EventType.RESOURCE_INVALIDATED
invalid_reasons: Iterable[str]
@dataclass(frozen=True)
class ResourceMissing(AbstractEvent):
type = EventType.RESOURCE_MISSING
expected_resource_path: str
class ResourceIncorrect(ResourceEvent):
type = EventType.RESOURCE_INCORRECT
class ResourceTransferSuccessful(ResourceEvent):
type = EventType.RESOURCE_TRANSFER_SUCCESSFUL
class ResourceTransferPartial(ResourceEvent):
type = EventType.RESOURCE_TRANSFER_PARTIAL
class ResourceTransferFailed(ResourceEvent):
type = EventType.RESOURCE_TRANSFER_FAILED
@dataclass(frozen=True)
class FilesystemEventReceived(AbstractEvent):
type = EventType.FILESYSTEM_EVENT_RECEIVED
event: FilesystemEventType
class StandardEventLogger(IEventLogger):
def __init__(self,
logger: ILogger,
info_logged: Tuple[EventType],
warning_logged: Tuple[EventType],
error_logged: Tuple[EventType],):
self._logger = logger
self._info_logged = info_logged
self._warning_logged = info_logged
self._error_logged = info_logged
def log(self, event: Event):
out_string_params = ""
for key, value in as_dict(event).items():
out_string_params += f"{key}: {value}, "
log_string = f"{event.type.name} occurred: {out_string_params}"
if event.type in (self._info_logged):
self._logger.info(log_string)
elif event.type in (self._warning_logged):
self._logger.warning(log_string)
elif event.type in (self._error_logged):
self._logger.error(log_string)
else:
raise ValueError(f"Unknown event {event.type}")
class WindowsEventLogger(IEventLogger):
def __init__(self, source_name: str):
self._source_name = name
def log(self, event: Event):
data_dict = asdict(event)
strings = [f"{key}: {value}" for key, value in data_dict.items()]
win32evtlogutil.ReportEvent(
self._source_name,
int(event.type),
eventType=win32evtlog.EVENTLOG_INFO_TYPE, # Selecting the event type based on the event.type
strings=strings
)
Additionally, I find it difficult to evaluate whether this satisfies the Open-Closed principle. I am not a huge fan of the design by intuition, as it is essentially an event handler that happens to log, so perhaps it would be better to just key it as an event handler and learn how a good way to raise events like this. I would love a resource to study, as on writing this it seems like a rather good idea. Then I can decorate an event handler with a logging event handler.
Additionally, perhaps its a better idea to allow the client of the event logger to define its log level. IE
class LogLevel(Enum):
INFO = auto()
WARNING = auto()
ERROR = auto()
class IEventLogger(ABC):
@abstractmethod
def log(self, event: AbstractEvent, level: LogLevel):
pass
I would love any feedback. I approach with the problem I want to solve and the expectation that you guys will show me something that I will learn from. I hope I am not shoehorning in design patterns, but I have found great success with the composite and adapter pattern and am finding the effective use of the decorator pattern extremely difficult.
(<info event types>)
executable? \$\endgroup\$