Skip to content

Commit 93232e1

Browse files
committed
Replace custom Singleton and SingletonMeta implementations with a unified design in mongoose.core, streamline imports, and adjust GeoIP configuration defaults.
1 parent 507219d commit 93232e1

10 files changed

Lines changed: 93 additions & 92 deletions

File tree

mongoose/core/__init__.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import threading
2+
from typing import Dict, Any
3+
4+
5+
class SingletonMeta(type):
6+
"""
7+
Thread-safe metaclass implementing a per-class singleton.
8+
9+
Each class that uses this metaclass will only ever have a single
10+
instance created. The first construction's args/kwargs are used to
11+
initialize the singleton; later constructions return the same
12+
instance and ignore new args.
13+
"""
14+
15+
_instances: Dict[type, Any] = {}
16+
_lock: threading.Lock = threading.Lock()
17+
18+
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
19+
# Double-checked locking to avoid acquiring the lock every time
20+
if cls not in cls._instances:
21+
with cls._lock:
22+
if cls not in cls._instances:
23+
instance = super().__call__(*args, **kwargs)
24+
cls._instances[cls] = instance
25+
return cls._instances[cls]
26+
27+
28+
class Singleton(type):
29+
_instances: Dict[type, type] = {}
30+
31+
def __call__(cls, *args, **kwargs):
32+
"""Control instance creation to ensure singleton behavior.
33+
34+
Args:
35+
cls (type): The class being instantiated
36+
*args: Positional arguments for class initialization
37+
**kwargs: Keyword arguments for class initialization
38+
39+
Returns:
40+
type: The singleton instance of the class
41+
42+
Note:
43+
If an instance already exists, ``__init__`` will still be called with
44+
the provided arguments, but no new instance is created.
45+
"""
46+
if cls not in cls._instances:
47+
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
48+
return cls._instances[cls]

mongoose/core/cache.py

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,14 @@
1-
from typing import Optional, Generic, TypeVar, Iterator, Dict, Any, Tuple, List
2-
from collections import OrderedDict
31
import threading
42
import time
3+
from collections import OrderedDict
4+
from typing import Optional, Generic, TypeVar, Iterator, Dict, Any, List
5+
6+
from mongoose.core import SingletonMeta
57

68
K = TypeVar("K")
79
V = TypeVar("V")
810

911

10-
class SingletonMeta(type):
11-
"""
12-
Thread-safe metaclass implementing a per-class singleton.
13-
14-
Each class that uses this metaclass will only ever have a single
15-
instance created. The first construction's args/kwargs are used to
16-
initialize the singleton; later constructions return the same
17-
instance and ignore new args.
18-
"""
19-
20-
_instances: Dict[type, Any] = {}
21-
_lock: threading.Lock = threading.Lock()
22-
23-
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
24-
# Double-checked locking to avoid acquiring the lock every time
25-
if cls not in cls._instances:
26-
with cls._lock:
27-
if cls not in cls._instances:
28-
instance = super().__call__(*args, **kwargs)
29-
cls._instances[cls] = instance
30-
return cls._instances[cls]
31-
32-
3312
class Cache(Generic[K, V], metaclass=SingletonMeta):
3413
"""
3514
Sharded LRU cache with optional TTL implemented as a singleton per class.

mongoose/core/engine.py

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import logging
22
import threading
3-
from typing import List, Optional, Type, Dict, Set, Any
3+
from typing import List, Optional, Type
44

55
import yaml
66

77
import mongoose.core.cache as cache_module
88
from mongoose.collect.nfstream_collector import NFStreamCollector
99
from mongoose.collect.suricata_eve_collector import SuricataEveCollector
10+
from mongoose.core import Singleton
1011
from mongoose.core.cache import SeverityCache
1112
from mongoose.core.processing import ProcessingQueue
13+
from mongoose.core.registry import JobRegistry
1214
from mongoose.core.sink import Sink
1315
from mongoose.core.watchdogs import DropInConfigurationWatcher, DropInConfigurationHandler
1416
from mongoose.enrich.base import Enrich
@@ -21,40 +23,6 @@
2123
logger = logging.getLogger(__name__)
2224

2325

24-
class Singleton(type):
25-
_instances: Dict[type, type] = {}
26-
27-
def __call__(cls, *args, **kwargs):
28-
"""Control instance creation to ensure singleton behavior.
29-
30-
Args:
31-
cls (type): The class being instantiated
32-
*args: Positional arguments for class initialization
33-
**kwargs: Keyword arguments for class initialization
34-
35-
Returns:
36-
type: The singleton instance of the class
37-
38-
Note:
39-
If an instance already exists, ``__init__`` will still be called with
40-
the provided arguments, but no new instance is created.
41-
"""
42-
if cls not in cls._instances:
43-
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
44-
return cls._instances[cls]
45-
46-
47-
class JobRegistry(metaclass=Singleton):
48-
def __init__(self):
49-
self.jobs: Set[Any] = set()
50-
51-
def register(self, job: Any):
52-
self.jobs.add(job)
53-
54-
def clear(self):
55-
self.jobs.clear()
56-
57-
5826
class Engine(metaclass=Singleton):
5927
"""
6028
The Engine class is responsible for loading the configuration,
@@ -78,9 +46,13 @@ def __init__(self, config_path: str, interface: str = None, watch_configuration_
7846
self.started = False
7947
self.thread_id = threading.get_ident()
8048
self.watch_configuration_changes = watch_configuration_changes
81-
self.webhook_configuration_watcher = DropInConfigurationWatcher(
82-
self.config.extra_configuration_dir / "webhook.d",
83-
DropInConfigurationHandler(WebhookForwarderConfiguration, self._handle_webhook_configuration_changes),
49+
self.webhook_configuration_watcher = (
50+
DropInConfigurationWatcher(
51+
self.config.extra_configuration_dir / "webhook.d",
52+
DropInConfigurationHandler(WebhookForwarderConfiguration, self._handle_webhook_configuration_changes),
53+
)
54+
if self.watch_configuration_changes
55+
else None
8456
)
8557

8658
def load_extra_config(self, name: str, config_class: Type):

mongoose/core/registry.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from typing import Set, Any
2+
3+
from mongoose.core import Singleton
4+
5+
6+
class JobRegistry(metaclass=Singleton):
7+
def __init__(self):
8+
self.jobs: Set[Any] = set()
9+
10+
def register(self, job: Any):
11+
self.jobs.add(job)
12+
13+
def clear(self):
14+
self.jobs.clear()

mongoose/enrich/__init__.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +0,0 @@
1-
from .base import Enrich
2-
from .community_id import CommunityIDEnrichment
3-
from .direction import DirectionEnrichment
4-
from .geoip import MaxMindGeoIP, IP66GeoIP
5-
from .hostname import HostnameEnrichment
6-
7-
__all__ = [
8-
"Enrich",
9-
"CommunityIDEnrichment",
10-
"DirectionEnrichment",
11-
"IP66GeoIP",
12-
"MaxMindGeoIP",
13-
"HostnameEnrichment",
14-
]

mongoose/enrich/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
from typing import Optional
66

77
from mongoose.core.processing import ProcessingQueue, ProcessingTopic
8-
from mongoose.enrich import *
8+
from mongoose.enrich.community_id import CommunityIDEnrichment
9+
from mongoose.enrich.direction import DirectionEnrichment
10+
from mongoose.enrich.geoip import MaxMindGeoIP, IP66GeoIP
11+
from mongoose.enrich.hostname import HostnameEnrichment
912
from mongoose.enrich.risk import FlowRiskEnrichment
1013
from mongoose.enrich.type import EventTypeEnrichment
1114
from mongoose.models import NetworkDPI, NetworkAlert, NetworkFlow
@@ -34,7 +37,7 @@ def __init__(self, enrichment_configuration: EnrichmentConfiguration):
3437
)
3538
if geoip_source and geoip_source.lower() == "maxmind":
3639
self.geoip_enrichment = MaxMindGeoIP(enrichment_configuration.geoip)
37-
else:
40+
elif geoip_source and geoip_source.lower() == "ip66":
3841
self.geoip_enrichment = IP66GeoIP(enrichment_configuration.geoip)
3942

4043
def start(self):

mongoose/enrich/geoip.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import ipaddress
21
import logging
2+
import ipaddress
33
from functools import lru_cache
4-
from typing import Union, Any, Dict, Optional
4+
from typing import Union, Optional, Dict, Any
5+
56
import geoip2
67
import geoip2.database
7-
88
import maxminddb
99

10-
from mongoose.core.engine import JobRegistry
10+
from mongoose.core.registry import JobRegistry
1111
from mongoose.models import NetworkDPI, NetworkFlow, NetworkAlert
1212
from mongoose.models.configuration import GeoIPConfiguration
1313
from mongoose.utils.exceptions import IgnoreCacheException

mongoose/models/configuration.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,11 @@ class GeoIPConfiguration(BaseModel):
189189
maxmind_db: List[str] = ["GeoLite2-ASN.mmdb", "GeoLite2-City.mmdb", "GeoLite2-Country.mmdb"]
190190
"""The list of GeoIP databases to use."""
191191

192-
source: str = "ip66" # maxmind or ip66
192+
source: str = "maxmind" # maxmind or ip66
193193
"""The source to use, either MaxMind or IP66. Defaults to IP66."""
194194

195-
enable: bool = Field(default=True)
196-
"""Enable the GeoIP enrichment. Defaults to True."""
195+
enable: bool = Field(default=False)
196+
"""Enable the GeoIP enrichment. Defaults to False."""
197197

198198

199199
class EnrichmentConfiguration(BaseModel):

mongoose/utils/jobs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def __init__(self, url, local_path, run_time="09:00"):
2929
def _parse_run_time(self, run_time_str):
3030
"""Parse run time string to hours and minutes."""
3131
try:
32-
hour, minute = map(int, run_time_str.split(':'))
32+
hour, minute = map(int, run_time_str.split(":"))
3333
if not (0 <= hour <= 23 and 0 <= minute <= 59):
3434
raise ValueError("Invalid time range")
3535
return hour, minute

tests/test_engine.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def test_engine_initialization(tmp_path):
1313
"suricata": {"socket_path": "/tmp/suricata.socket", "enable": True},
1414
"nf_stream": {"interface": "eth0", "enable": False},
1515
},
16-
"enrichment": {"geoip": {"remote_service_url": "http://geoip", "enable": True}},
16+
"enrichment": {"geoip": {"enable": False}},
1717
"forwarder": {
1818
"file": {"output_dir": str(tmp_path / "output"), "enable": True},
1919
"webhooks": [{"url": "http://webhook", "enable": False}],
@@ -31,17 +31,16 @@ def test_engine_initialization(tmp_path):
3131

3232

3333
@patch("mongoose.collect.suricata_eve_collector.SuricataEveCollector.start")
34-
@patch("mongoose.enrich.Enrich.start")
34+
@patch("mongoose.enrich.base.Enrich.start")
3535
@patch("mongoose.forward.file.FileForwarder.start")
36-
@patch("mongoose.enrich.Enrich.__init__", return_value=None)
37-
def test_engine_start(mock_enrich_init, mock_file_start, mock_enrich_start, mock_suricata_start, tmp_path):
36+
def test_engine_start(mock_file_start, mock_enrich_start, mock_suricata_start, tmp_path):
3837
config_content = {
3938
"configuration": {
4039
"collector": {
4140
"suricata": {"socket_path": "/tmp/suricata.socket", "enable": True},
4241
"nf_stream": {"interface": "eth0", "enable": False},
4342
},
44-
"enrichment": {"geoip": {"enable": True}},
43+
"enrichment": {"geoip": {"enable": False}},
4544
"forwarder": {
4645
"file": {"output_dir": str(tmp_path / "output"), "enable": True},
4746
"webhooks": [{"url": "http://webhook", "enable": False}],

0 commit comments

Comments
 (0)