Upgraded version

This commit is contained in:
2025-10-16 00:03:31 +02:00
parent 0b5559be69
commit c67b2b54c8
2 changed files with 134 additions and 79 deletions

View File

@@ -1,43 +1,72 @@
{ {
"log-fetcher":{ "log-fetchers": [
"type": "loki", {
"config": { "name": "loki-fileserver",
"url": "http://fileserver.home:3100" "type": "loki",
"config": {
"url": "http://fileserver.home:3100"
}
} }
}, ],
"alert-manager":{ "alert-managers":[
"type": "gotify", {
"config": { "name": "gotify-paris",
"url": "https://paris.nappez.com/gotify/message", "type": "gotify",
"token-from-env": "{GOTIFY_TOKEN}" "config": {
"url": "https://paris.nappez.com/gotify/message",
"token-from-env": "{GOTIFY_TOKEN}"
}
} }
}, ],
"check-interval": 60,
"log-alerts": [ "log-alerts": [
{ {
"name": "SSH outside connection", "name": "SSH outside connection",
"filters": { "check-interval": 60,
"labels": { "log-fetcher": {
"container": "openssh-server" "name": "loki-fileserver",
}, "filters": {
"text": "Accepted", "labels": {
"match": "Accepted (?P<method>\\w+) for (?P<username>\\w+) from (?P<ip>[^\\s]+)" "container": "openssh-server"
},
"text": "Accepted"
}
}, },
"alert": { "filters": [
{
"type": "regexp",
"config": {
"match": "Accepted (?P<method>\\w+) for (?P<username>\\w+) from (?P<ip>[^\\s]+)"
}
}
],
"alert-manager": {
"name": "gotify-paris",
"title": "Outside SSH login", "title": "Outside SSH login",
"message": "New SSH login for {username} on {instance} from ip {ip} (method: {method})" "message": "New SSH login for {username} on {instance} from ip {ip} (method: {method})"
} }
}, },
{ {
"name": "SSH local connection", "name": "SSH local connection",
"filters": { "check-interval": 30,
"labels": { "log-fetcher": {
"filename": "/var/log/host/auth.log" "name": "loki-fileserver",
}, "filters": {
"text": "Accepted", "labels": {
"match": "Accepted (?P<method>\\w+) for (?P<username>\\w+) from (?P<ip>[^\\s]+)" "filename": "/var/log/host/auth.log"
},
"text": "Accepted"
}
}, },
"alert": { "filters": [
{
"type": "regexp",
"config": {
"match": "Accepted (?P<method>\\w+) for (?P<username>\\w+) from (?P<ip>[^\\s]+)"
}
}
],
"alert-manager": {
"name": "gotify-paris",
"title": "Local SSH login", "title": "Local SSH login",
"message": "New SSH login for {username} on {instance} from ip {ip} (method: {method})" "message": "New SSH login for {username} on {instance} from ip {ip} (method: {method})"
} }

View File

@@ -16,6 +16,13 @@ class LogFetcher(ABC):
def fetch_logs(self, filters: Dict[str, Any], start_time: int, end_time: int) -> List[Dict[str, Any]]: def fetch_logs(self, filters: Dict[str, Any], start_time: int, end_time: int) -> List[Dict[str, Any]]:
pass pass
class Filter(ABC):
"""Abstract base class for filters."""
@abstractmethod
def filter(self, log: Dict[str, Any]) -> Dict[str, Any]:
pass
class AlertManager(ABC): class AlertManager(ABC):
"""Abstract base class for alert managers.""" """Abstract base class for alert managers."""
@@ -44,8 +51,8 @@ class LokiLogFetcher(LogFetcher):
payload = { payload = {
"query": query, "query": query,
"limit": 1000, "limit": 1000,
"start": str(start_time * 1000000000), # Convert to nanoseconds "start": str(int(start_time) * 1000000000), # Convert to nanoseconds
"end": str(end_time * 1000000000), "end": str(int(end_time) * 1000000000),
"direction": "forward" "direction": "forward"
} }
try: try:
@@ -58,13 +65,29 @@ class LokiLogFetcher(LogFetcher):
timestamp, log = value timestamp, log = value
logs.append({ logs.append({
"timestamp": timestamp, "timestamp": timestamp,
"log": log "log": log,
} | stream.get("stream", {})) "labels": stream.get("stream", {})
})
return logs return logs
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
print(f"Error fetching logs from Loki: {e}") print(f"Error fetching logs from Loki: {e}")
return [] return []
# Regexp Filter
class RegexpFilter(Filter):
"""Concrete implementation for Regexp filter."""
def __init__(self, config: Dict[str, Any]):
self.match = config["match"]
def filter(self, log: Dict[str, Any]) -> Dict[str, Any]:
match = re.search(self.match, log["log"])
print(f"Regex match for '{self.match}' in log: {match.groupdict()}")
if match:
log["labels"].update(match.groupdict())
return log
return None
# Gotify Alert Manager # Gotify Alert Manager
class GotifyAlertManager(AlertManager): class GotifyAlertManager(AlertManager):
"""Concrete implementation for Gotify alert manager.""" """Concrete implementation for Gotify alert manager."""
@@ -91,19 +114,39 @@ class GotifyAlertManager(AlertManager):
class AlertRule: class AlertRule:
"""Represents an alert rule with filters and alert template.""" """Represents an alert rule with filters and alert template."""
def __init__(self, config: Dict[str, Any]): def __init__(self, log_fetchers: LogFetcher, alert_managers: AlertManager, config: Dict[str, Any]):
self.name = config["name"] self.name = config["name"]
self.filters = config["filters"] self.log_fetcher = log_fetchers[config["log-fetcher"]["name"]]
self.alert_title = config["alert"]["title"] self.fetcher_filters = config["log-fetcher"].get("filters", {})
self.alert_message = config["alert"]["message"] self.check_interval = config.get("check-interval", 60)
self.filters = []
for filter in config.get("filters", []):
if filter["type"] == "regexp":
self.filters.append(RegexpFilter(filter["config"]))
else:
raise ValueError(f"Unsupported filter type: {filter['type']}")
self.alert_manager = alert_managers[config["alert-manager"]["name"]]
self.alert_title = config["alert-manager"]["title"]
self.alert_message = config["alert-manager"]["message"]
self.last_run = time.time() - self.check_interval
self.next_run = time.time()
def matches(self, log: str) -> Optional[Dict[str, str]]: def run(self) -> None:
"""Check if the log matches the alert rule.""" print(f"Processing rule: {self.name}")
match = re.search(self.filters.get("match"), log) logs = self.log_fetcher.fetch_logs(self.fetcher_filters, self.last_run, self.next_run)
print(f"Regex match for '{self.filters.get('match')}' in log: {match.groupdict()}") for log_entry in logs:
if match: print(f"Checking log: {log_entry['log']}")
return match.groupdict() for filter in self.filters:
return None log_entry = filter.filter(log_entry)
if log_entry is None:
break
if log_entry is None:
continue
message = self.alert_message.format_map(log_entry["labels"])
print(f"Sending message: {message}, with params: {log_entry}")
self.alert_manager.send_alert(self.alert_title, message)
self.last_run = self.next_run
self.next_run = time.time() + self.check_interval
# Main Application # Main Application
class LogAlertApp: class LogAlertApp:
@@ -111,9 +154,14 @@ class LogAlertApp:
def __init__(self, config_path: str): def __init__(self, config_path: str):
self.config = self._load_config(config_path) self.config = self._load_config(config_path)
self.log_fetcher = self._init_log_fetcher() print(f"Configuration loaded: {self.config}")
self.alert_manager = self._init_alert_manager() self.log_fetchers = {}
self.alert_rules = [AlertRule(rule) for rule in self.config["log-alerts"]] for fetcher in self.config["log-fetchers"]:
self.log_fetchers[fetcher["name"]] = self._init_log_fetcher(fetcher)
self.alert_managers = {}
for manager in self.config["alert-managers"]:
self.alert_managers[manager["name"]] = self._init_alert_manager(manager)
self.alert_rules = [AlertRule(self.log_fetchers, self.alert_managers, rule) for rule in self.config["log-alerts"]]
def _load_config(self, config_path: str) -> Dict[str, Any]: def _load_config(self, config_path: str) -> Dict[str, Any]:
"""Load the configuration from a JSON file.""" """Load the configuration from a JSON file."""
@@ -132,66 +180,44 @@ class LogAlertApp:
for key, value in config.items(): for key, value in config.items():
if isinstance(value, dict): if isinstance(value, dict):
config[key] = self._update_config_from_env(value) config[key] = self._update_config_from_env(value)
elif isinstance(value, list):
config[key] = [self._update_config_from_env(item) for item in value]
elif isinstance(value, str) and key.endswith("-from-env"): elif isinstance(value, str) and key.endswith("-from-env"):
config[key[0:-9]] = value.format_map(os.environ) new_key = key[:-9] # Remove '-from-env'
config[new_key] = value.format_map(os.environ)
del config[key] del config[key]
return self._update_config_from_env(config) # re-evaluate in case of nested env vars return self._update_config_from_env(config) # re-evaluate in case of nested env vars
return config return config
def _init_log_fetcher(self) -> LogFetcher: def _init_log_fetcher(self, fetcher_config: Dict[str, Any]) -> LogFetcher:
"""Initialize the log fetcher based on config.""" """Initialize the log fetcher based on config."""
fetcher_config = self.config["log-fetcher"]
if fetcher_config["type"] == "loki": if fetcher_config["type"] == "loki":
return LokiLogFetcher(fetcher_config["config"]) return LokiLogFetcher(fetcher_config["config"])
else: else:
raise ValueError(f"Unsupported log fetcher type: {fetcher_config['type']}") raise ValueError(f"Unsupported log fetcher type: {fetcher_config['type']}")
def _init_alert_manager(self) -> AlertManager: def _init_alert_manager(self, manager_config: Dict[str, Any]) -> AlertManager:
"""Initialize the alert manager based on config.""" """Initialize the alert manager based on config."""
manager_config = self.config["alert-manager"]
if manager_config["type"] == "gotify": if manager_config["type"] == "gotify":
return GotifyAlertManager(manager_config["config"]) return GotifyAlertManager(manager_config["config"])
else: else:
raise ValueError(f"Unsupported alert manager type: {manager_config['type']}") raise ValueError(f"Unsupported alert manager type: {manager_config['type']}")
def run_once(self, start_time: int, end_time: int) -> None: def run(self) -> None:
for rule in self.alert_rules:
print(f"Processing rule: {rule.name}")
logs = self.log_fetcher.fetch_logs(rule.filters, start_time, end_time)
for log_entry in logs:
print(f"Checking log: {log_entry['log']}")
match = rule.matches(log_entry["log"])
log_entry.update(match)
if match:
message = rule.alert_message.format_map(log_entry)
print(f"Sending message: {message}, with params: {log_entry}")
self.alert_manager.send_alert(rule.alert_title, message)
def run(self, start_time: int, end_time: int) -> None:
"""Fetch logs, check for matches, and send alerts.""" """Fetch logs, check for matches, and send alerts."""
if start_time == 0 and end_time == 0: while True:
last_run = int(time.time())-self.config["check-interval"]; for rule in self.alert_rules:
while True: if time.time() >= rule.next_run:
now = int(time.time()) rule.run()
self.run_once(last_run, now) time.sleep(5)
last_run = now
time.sleep(self.config["check-interval"])
else:
if start_time == 0:
start_time = int(time.time()) - self.config["check-interval"]
if end_time == 0:
end_time = int(time.time())
self.run_once(start_time, end_time)
def main(): def main():
parser = argparse.ArgumentParser(description="Log and Alert Management Tool") parser = argparse.ArgumentParser(description="Log and Alert Management Tool")
parser.add_argument("--config", required=True, help="Path to the configuration file") parser.add_argument("--config", required=True, help="Path to the configuration file")
parser.add_argument("--start", type=int, default=0, help="Start time (Unix timestamp)")
parser.add_argument("--end", type=int, default=0, help="End time (Unix timestamp)")
args = parser.parse_args() args = parser.parse_args()
app = LogAlertApp(args.config) app = LogAlertApp(args.config)
app.run(args.start, args.end) app.run()
if __name__ == "__main__": if __name__ == "__main__":
main() main()