Initial commit: LogHive centralized log management system
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""Alert evaluation and notification service."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.models.alert import AlertRule, AlertHistory, AlertLevel, AlertOperator
|
||||
from app.models.log import LogEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AlertService:
|
||||
"""Evaluates alert rules against recent log data and triggers notifications."""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def evaluate_rules(self) -> list[AlertHistory]:
|
||||
"""Check all enabled alert rules and trigger any that match."""
|
||||
result = await self.db.execute(
|
||||
select(AlertRule).where(AlertRule.is_enabled == True)
|
||||
)
|
||||
rules: list[AlertRule] = result.scalars().all()
|
||||
triggered: list[AlertHistory] = []
|
||||
|
||||
for rule in rules:
|
||||
try:
|
||||
hit = await self._evaluate_single(rule)
|
||||
if hit:
|
||||
triggered.append(hit)
|
||||
except Exception as e:
|
||||
logger.error("Error evaluating rule %s: %s", rule.id, e)
|
||||
|
||||
return triggered
|
||||
|
||||
async def _evaluate_single(self, rule: AlertRule) -> Optional[AlertHistory]:
|
||||
"""Evaluate a single rule. Returns AlertHistory if triggered."""
|
||||
now = datetime.now(timezone.utc)
|
||||
since = now - timedelta(minutes=rule.window_minutes)
|
||||
|
||||
stmt = (
|
||||
select(func.count(LogEntry.id))
|
||||
.where(LogEntry.project_id == rule.project_id)
|
||||
.where(LogEntry.timestamp >= since)
|
||||
.where(LogEntry.level == rule.level.value)
|
||||
)
|
||||
count = (await self.db.execute(stmt)).scalar() or 0
|
||||
|
||||
operator_map = {
|
||||
AlertOperator.GT: lambda v, t: v > t,
|
||||
AlertOperator.GTE: lambda v, t: v >= t,
|
||||
AlertOperator.LT: lambda v, t: v < t,
|
||||
AlertOperator.LTE: lambda v, t: v <= t,
|
||||
AlertOperator.EQ: lambda v, t: v == t,
|
||||
}
|
||||
|
||||
op_fn = operator_map.get(rule.operator)
|
||||
if op_fn and op_fn(float(count), rule.threshold):
|
||||
history = AlertHistory(
|
||||
rule_id=rule.id,
|
||||
project_id=rule.project_id,
|
||||
level=rule.level,
|
||||
message=(
|
||||
f"Alert '{rule.name}': {rule.level.value} logs count ({count}) "
|
||||
f"{rule.operator.value} threshold ({rule.threshold}) "
|
||||
f"in last {rule.window_minutes} min"
|
||||
),
|
||||
triggered_value=float(count),
|
||||
)
|
||||
self.db.add(history)
|
||||
await self.db.flush()
|
||||
logger.info("Alert triggered: %s", history.message)
|
||||
|
||||
if rule.notify_channels:
|
||||
await self._notify(rule, history)
|
||||
|
||||
return history
|
||||
|
||||
return None
|
||||
|
||||
async def _notify(self, rule: AlertRule, history: AlertHistory) -> None:
|
||||
"""Send notification via configured channels."""
|
||||
channels = [c.strip() for c in rule.notify_channels.split(",") if c.strip()]
|
||||
for channel in channels:
|
||||
if channel == "feishu" and settings.FEISHU_WEBHOOK_URL:
|
||||
await self._send_feishu(rule, history)
|
||||
elif channel == "dingtalk" and settings.DINGTALK_WEBHOOK_URL:
|
||||
await self._send_dingtalk(rule, history)
|
||||
|
||||
async def _send_feishu(self, rule: AlertRule, history: AlertHistory) -> None:
|
||||
"""Send a notification to Feishu webhook."""
|
||||
import httpx
|
||||
|
||||
payload = {
|
||||
"msg_type": "post",
|
||||
"content": {
|
||||
"post": {
|
||||
"zh_cn": {
|
||||
"title": f"[LogHive] Alert: {rule.name}",
|
||||
"content": [
|
||||
[{"tag": "text", "text": history.message}],
|
||||
[
|
||||
{
|
||||
"tag": "text",
|
||||
"text": f"Project: {rule.project_id} | Level: {rule.level.value}",
|
||||
}
|
||||
],
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.post(settings.FEISHU_WEBHOOK_URL, json=payload, timeout=10)
|
||||
except Exception as e:
|
||||
logger.error("Feishu notification failed: %s", e)
|
||||
|
||||
async def _send_dingtalk(self, rule: AlertRule, history: AlertHistory) -> None:
|
||||
"""Send a notification to DingTalk webhook."""
|
||||
import httpx
|
||||
|
||||
payload = {
|
||||
"msgtype": "text",
|
||||
"text": {
|
||||
"content": (
|
||||
f"[LogHive] Alert: {rule.name}\n"
|
||||
f"{history.message}\n"
|
||||
f"Project: {rule.project_id} | Level: {rule.level.value}"
|
||||
)
|
||||
},
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.post(settings.DINGTALK_WEBHOOK_URL, json=payload, timeout=10)
|
||||
except Exception as e:
|
||||
logger.error("DingTalk notification failed: %s", e)
|
||||
Reference in New Issue
Block a user