mirror of
https://github.com/napnap75/multiarch-docker-images.git
synced 2025-12-16 03:34:18 +01:00
Add jsonschema validation (#6)
* Add JSON Schema validation for log-alert config and require jsonschema * Add jsonschema dependency to requirements * Enhance config loading with schema validation Added JSON schema validation for configuration and improved error handling. * Simplify token assignment and config loading Refactor token retrieval and configuration loading.
This commit is contained in:
115
log-alert/config.schema.json
Normal file
115
log-alert/config.schema.json
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Log Alert configuration schema",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["log-fetchers", "alert-managers", "log-alerts"],
|
||||||
|
"properties": {
|
||||||
|
"log-fetchers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "type", "config"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"type": { "type": "string", "enum": ["loki"] },
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": { "type": "string", "format": "uri" }
|
||||||
|
},
|
||||||
|
"required": ["url"],
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alert-managers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "type", "config"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"type": { "type": "string", "enum": ["gotify"] },
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": { "type": "string", "format": "uri" },
|
||||||
|
"token": { "type": "string" },
|
||||||
|
"token-from-env": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["url"],
|
||||||
|
"anyOf": [
|
||||||
|
{ "required": ["token"] },
|
||||||
|
{ "required": ["token-from-env"] }
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"log-alerts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "log-fetcher", "filters", "alert-manager"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"check-interval": { "type": "number", "minimum": 0 },
|
||||||
|
"log-fetcher": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"filters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"labels": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "type": "string" }
|
||||||
|
},
|
||||||
|
"text": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "config"],
|
||||||
|
"properties": {
|
||||||
|
"type": { "type": "string", "enum": ["regexp"] },
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["match"],
|
||||||
|
"properties": {
|
||||||
|
"match": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alert-manager": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"title": { "type": "string" },
|
||||||
|
"message": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import jsonschema
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
@@ -7,7 +9,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
# Base Classes
|
# Base Classes
|
||||||
class LogFetcher(ABC):
|
class LogFetcher(ABC):
|
||||||
"""Abstract base class for log fetchers."""
|
"""Abstract base class for log fetchers."""
|
||||||
@@ -82,10 +84,15 @@ class RegexpFilter(Filter):
|
|||||||
|
|
||||||
def filter(self, log: Dict[str, Any]) -> Dict[str, Any]:
|
def filter(self, log: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
match = re.search(self.match, log["log"])
|
match = re.search(self.match, log["log"])
|
||||||
print(f"Regex match for '{self.match}' in log: {match.groupdict()}")
|
|
||||||
if match:
|
if match:
|
||||||
log["labels"].update(match.groupdict())
|
# Only call groupdict() when there is a match
|
||||||
|
groups = match.groupdict()
|
||||||
|
print(f"Regex match for '{self.match}' in log: {groups}")
|
||||||
|
if groups:
|
||||||
|
log.setdefault("labels", {}).update(groups)
|
||||||
return log
|
return log
|
||||||
|
# no match
|
||||||
|
print(f"Regex did not match for pattern '{self.match}' in log: {log.get('log')}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Gotify Alert Manager
|
# Gotify Alert Manager
|
||||||
@@ -94,7 +101,7 @@ class GotifyAlertManager(AlertManager):
|
|||||||
|
|
||||||
def __init__(self, config: Dict[str, Any]):
|
def __init__(self, config: Dict[str, Any]):
|
||||||
self.url = config["url"]
|
self.url = config["url"]
|
||||||
self.token = config["token"]
|
self.token = config.get("token")
|
||||||
|
|
||||||
def send_alert(self, title: str, message: str) -> None:
|
def send_alert(self, title: str, message: str) -> None:
|
||||||
"""Send an alert to Gotify."""
|
"""Send an alert to Gotify."""
|
||||||
@@ -142,7 +149,7 @@ class AlertRule:
|
|||||||
break
|
break
|
||||||
if log_entry is None:
|
if log_entry is None:
|
||||||
continue
|
continue
|
||||||
message = self.alert_message.format_map(log_entry["labels"])
|
message = self.alert_message.format_map(log_entry.get("labels", {}))
|
||||||
print(f"Sending message: {message}, with params: {log_entry}")
|
print(f"Sending message: {message}, with params: {log_entry}")
|
||||||
self.alert_manager.send_alert(self.alert_title, message)
|
self.alert_manager.send_alert(self.alert_title, message)
|
||||||
self.last_run = self.next_run
|
self.last_run = self.next_run
|
||||||
@@ -162,12 +169,17 @@ class LogAlertApp:
|
|||||||
for manager in self.config["alert-managers"]:
|
for manager in self.config["alert-managers"]:
|
||||||
self.alert_managers[manager["name"]] = self._init_alert_manager(manager)
|
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"]]
|
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 and validate it with JSON Schema."""
|
||||||
try:
|
try:
|
||||||
with open(config_path, 'r') as config_file:
|
with open(config_path, 'r') as config_file:
|
||||||
return self._update_config_from_env(json.load(config_file))
|
# read JSON first
|
||||||
|
config = json.load(config_file)
|
||||||
|
# Perform schema validation if jsonschema is available
|
||||||
|
self._validate_config_with_schema(config)
|
||||||
|
# Update config to load env variable where required
|
||||||
|
return self._update_config_from_env(config)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print(f"Error: Configuration file '{config_path}' not found.")
|
print(f"Error: Configuration file '{config_path}' not found.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -175,9 +187,26 @@ class LogAlertApp:
|
|||||||
print(f"Error: Invalid JSON in configuration file '{config_path}'.")
|
print(f"Error: Invalid JSON in configuration file '{config_path}'.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
def _validate_config_with_schema(self, config: Dict[str, Any]) -> None:
|
||||||
|
"""Validate a loaded config dict against log-alert/config.schema.json if jsonschema is installed."""
|
||||||
|
schema_path = os.path.join(os.path.dirname(__file__), 'config.schema.json')
|
||||||
|
try:
|
||||||
|
with open(schema_path, 'r') as sf:
|
||||||
|
schema = json.load(sf)
|
||||||
|
jsonschema.validate(instance=config, schema=schema)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Warning: Schema file '{schema_path}' not found; skipping config validation.")
|
||||||
|
except jsonschema.exceptions.ValidationError as e:
|
||||||
|
print(f"Configuration validation error: {e.message}")
|
||||||
|
print("Detailed error:", e)
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error while validating configuration: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def _update_config_from_env(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
def _update_config_from_env(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Update config values from environment variables if specified."""
|
"""Update config values from environment variables if specified."""
|
||||||
for key, value in config.items():
|
for key, value in list(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):
|
elif isinstance(value, list):
|
||||||
@@ -210,7 +239,7 @@ class LogAlertApp:
|
|||||||
if time.time() >= rule.next_run:
|
if time.time() >= rule.next_run:
|
||||||
rule.run()
|
rule.run()
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
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")
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
requests
|
requests
|
||||||
|
jsonschema>=4.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user