4
\$\begingroup\$

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.

\$\endgroup\$
2
  • \$\begingroup\$ is (<info event types>) executable? \$\endgroup\$
    – lukstru
    Commented Jul 14, 2022 at 4:04
  • \$\begingroup\$ @lukstru info event types are a list/tuple of EventType. I will add this to the question. \$\endgroup\$ Commented Jul 14, 2022 at 4:21

0