Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/detectmatelibrary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@
from . import parsers
from . import schemas
from . import utils
from .exceptions import (
DetectMateError,
ComponentRunError,
DetectorRunError,
ParserRunError,
)

__all__ = [
"common",
"detectors",
"parsers",
"schemas",
"utils"
"utils",
"DetectMateError",
"ComponentRunError",
"DetectorRunError",
"ParserRunError",
]
13 changes: 13 additions & 0 deletions src/detectmatelibrary/common/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from detectmatelibrary.exceptions import (
DetectMateError,
ComponentRunError,
DetectorRunError,
ParserRunError,
)

__all__ = [
"DetectMateError",
"ComponentRunError",
"DetectorRunError",
"ParserRunError",
]
10 changes: 9 additions & 1 deletion src/detectmatelibrary/common/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from detectmatelibrary.common._config import BasicConfig

from detectmatelibrary.schemas import BaseSchema
from detectmatelibrary.exceptions import ComponentRunError, DetectMateError

from tools.logging import logger, setup_logging

Expand Down Expand Up @@ -109,7 +110,14 @@ def process(self, data: BaseSchema | bytes) -> BaseSchema | bytes | None:

output_ = self.output_schema()
logger.info(f"<<{self.name}>> processing data")
return_schema = self.run(input_=data_buffered, output_=output_)
try:
return_schema = self.run(input_=data_buffered, output_=output_)
except DetectMateError:
raise
except Exception as exc:
raise ComponentRunError(
f"Component '{self.name}' raised an error in run(): {exc}"
) from exc
if not return_schema:
logger.info(f"<<{self.name}>> returns None")
return None
Expand Down
10 changes: 9 additions & 1 deletion src/detectmatelibrary/common/detector.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from detectmatelibrary.common._config._formats import EventsConfig, _EventInstance
from detectmatelibrary.common.core import CoreComponent, CoreConfig
from detectmatelibrary.exceptions import DetectorRunError

from detectmatelibrary.utils.data_buffer import ArgsBuffer, BufferMode
from detectmatelibrary.utils.aux import get_timestamp
Expand Down Expand Up @@ -171,7 +172,14 @@ def run(
output_["extractedTimestamps"] = _extract_timestamp(input_)
output_["receivedTimestamp"] = get_timestamp()

if (anomaly_detected := self.detect(input_=input_, output_=output_)):
try:
anomaly_detected = self.detect(input_=input_, output_=output_)
except Exception as exc:
raise DetectorRunError(
f"Detector '{self.name}' raised an error in detect(): {exc}"
) from exc

if anomaly_detected:
output_["alertID"] = str(self.id_generator())
output_["detectionTimestamp"] = get_timestamp()

Expand Down
8 changes: 7 additions & 1 deletion src/detectmatelibrary/common/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from detectmatelibrary.utils.data_buffer import ArgsBuffer, BufferMode
from detectmatelibrary.common.core import CoreComponent, CoreConfig
from detectmatelibrary.utils.aux import get_timestamp
from detectmatelibrary.exceptions import ParserRunError
from detectmatelibrary import schemas

from typing import Any, Optional, cast
Expand Down Expand Up @@ -70,7 +71,12 @@ def run(self, input_: schemas.LogSchema, output_: schemas.ParserSchema) -> bool:
input_["log"] = content

output_["receivedTimestamp"] = get_timestamp()
use_schema = self.parse(input_=input_, output_=output_)
try:
use_schema = self.parse(input_=input_, output_=output_)
except Exception as exc:
raise ParserRunError(
f"Parser '{self.name}' raised an error in parse(): {exc}"
) from exc
output_["parsedTimestamp"] = get_timestamp()

return True if use_schema is None else use_schema
Expand Down
34 changes: 34 additions & 0 deletions src/detectmatelibrary/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Custom exceptions for DetectMateLibrary.

Use these exceptions in calling code to distinguish library-level errors
from unexpected failures and provide meaningful messages to users.
"""


class DetectMateError(Exception):
"""Base exception for all DetectMateLibrary errors."""


class ComponentRunError(DetectMateError):
"""Raised when an unhandled error occurs inside a component's run() method.

Wraps the original exception as ``__cause__`` so the full traceback is
preserved while still allowing callers to catch library errors by type.
"""


class DetectorRunError(ComponentRunError):
"""Raised when an unhandled error occurs inside a detector's detect() method.

Typically indicates a misconfiguration (e.g. an EventID present in the
incoming data that is not covered by the detector configuration) or a
bug in a custom detector implementation.
"""


class ParserRunError(ComponentRunError):
"""Raised when an unhandled error occurs inside a parser's parse() method.

Typically indicates a format mismatch between the incoming raw log and
the configured log format, or a bug in a custom parser implementation.
"""
169 changes: 169 additions & 0 deletions tests/test_common/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""Tests for meaningful exception wrapping in component run/detect/parse methods."""
import pytest

from detectmatelibrary.common.core import CoreComponent, CoreConfig
from detectmatelibrary.common.detector import CoreDetector, CoreDetectorConfig
from detectmatelibrary.common.parser import CoreParser, CoreParserConfig
from detectmatelibrary.exceptions import (
ComponentRunError,
DetectMateError,
DetectorRunError,
ParserRunError,
)
import detectmatelibrary.schemas as schemas


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

class _ErrorInRun(CoreComponent):
"""Component whose run() always raises a bare KeyError."""

def __init__(self) -> None:
super().__init__(name="ErrorInRun", type_="Dummy", config=CoreConfig(),
input_schema=schemas.LogSchema)

def run(self, input_, output_) -> bool:
raise KeyError("missing_key")


class _ErrorInDetect(CoreDetector):
"""Detector whose detect() always raises a bare KeyError."""

def __init__(self) -> None:
super().__init__(name="ErrorInDetect", config=CoreDetectorConfig())

def detect(self, input_, output_) -> bool:
raise KeyError("bad_event_id")


class _ErrorInParse(CoreParser):
"""Parser whose parse() always raises a bare ValueError."""

def __init__(self) -> None:
super().__init__(name="ErrorInParse", config=CoreParserConfig())

def parse(self, input_, output_) -> bool:
raise ValueError("unrecognised_format")


_log = schemas.LogSchema({"logID": "1", "log": "test log"})
_parser_schema = schemas.ParserSchema({
"parserType": "a",
"EventID": 0,
"template": "asd",
"variables": [""],
"logID": "0",
"parsedLogID": "22",
"parserID": "test",
"log": "This is a parsed log.",
"logFormatVariables": {"Time": "12121.12"},
})


# ---------------------------------------------------------------------------
# Exception hierarchy
# ---------------------------------------------------------------------------

class TestExceptionHierarchy:
def test_detector_run_error_is_component_run_error(self) -> None:
assert issubclass(DetectorRunError, ComponentRunError)

def test_parser_run_error_is_component_run_error(self) -> None:
assert issubclass(ParserRunError, ComponentRunError)

def test_component_run_error_is_detectmate_error(self) -> None:
assert issubclass(ComponentRunError, DetectMateError)

def test_detectmate_error_is_exception(self) -> None:
assert issubclass(DetectMateError, Exception)


# ---------------------------------------------------------------------------
# ComponentRunError wrapping
# ---------------------------------------------------------------------------

class TestComponentRunError:
def test_bare_exception_in_run_is_wrapped(self) -> None:
component = _ErrorInRun()
with pytest.raises(ComponentRunError):
component.process(_log)

def test_original_cause_is_preserved(self) -> None:
component = _ErrorInRun()
with pytest.raises(ComponentRunError) as exc_info:
component.process(_log)
assert isinstance(exc_info.value.__cause__, KeyError)

def test_component_name_in_message(self) -> None:
component = _ErrorInRun()
with pytest.raises(ComponentRunError) as exc_info:
component.process(_log)
assert "ErrorInRun" in str(exc_info.value)


# ---------------------------------------------------------------------------
# DetectorRunError wrapping
# ---------------------------------------------------------------------------

class TestDetectorRunError:
def test_bare_exception_in_detect_is_wrapped(self) -> None:
detector = _ErrorInDetect()
with pytest.raises(DetectorRunError):
detector.process(_parser_schema)

def test_original_cause_is_preserved(self) -> None:
detector = _ErrorInDetect()
with pytest.raises(DetectorRunError) as exc_info:
detector.process(_parser_schema)
assert isinstance(exc_info.value.__cause__, KeyError)

def test_detector_name_in_message(self) -> None:
detector = _ErrorInDetect()
with pytest.raises(DetectorRunError) as exc_info:
detector.process(_parser_schema)
assert "ErrorInDetect" in str(exc_info.value)

def test_detector_run_error_catchable_as_component_run_error(self) -> None:
detector = _ErrorInDetect()
with pytest.raises(ComponentRunError):
detector.process(_parser_schema)

def test_detector_run_error_catchable_as_detectmate_error(self) -> None:
detector = _ErrorInDetect()
with pytest.raises(DetectMateError):
detector.process(_parser_schema)


# ---------------------------------------------------------------------------
# ParserRunError wrapping
# ---------------------------------------------------------------------------

class TestParserRunError:
def test_bare_exception_in_parse_is_wrapped(self) -> None:
parser = _ErrorInParse()
with pytest.raises(ParserRunError):
parser.process(_log)

def test_original_cause_is_preserved(self) -> None:
parser = _ErrorInParse()
with pytest.raises(ParserRunError) as exc_info:
parser.process(_log)
assert isinstance(exc_info.value.__cause__, ValueError)

def test_parser_name_in_message(self) -> None:
parser = _ErrorInParse()
with pytest.raises(ParserRunError) as exc_info:
parser.process(_log)
assert "ErrorInParse" in str(exc_info.value)

def test_parser_run_error_catchable_as_component_run_error(self) -> None:
parser = _ErrorInParse()
with pytest.raises(ComponentRunError):
parser.process(_log)

def test_parser_run_error_catchable_as_detectmate_error(self) -> None:
parser = _ErrorInParse()
with pytest.raises(DetectMateError):
parser.process(_log)
4 changes: 3 additions & 1 deletion tests/test_pipelines/test_bad_players.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from detectmatelibrary.common.parser import CoreParser

import detectmatelibrary.schemas._classes as schema_classes
from detectmatelibrary.exceptions import ComponentRunError
from detectmatelibrary.helper.from_to import From

import pytest
Expand Down Expand Up @@ -66,5 +67,6 @@ def test_get_incorrect_schema(self) -> None:
buffer_size=None,
)

with pytest.raises(schema_classes.FieldNotFound):
with pytest.raises(ComponentRunError) as exc_info:
next(From.log(detector, log_path))
assert isinstance(exc_info.value.__cause__, schema_classes.FieldNotFound)