feat:更新日志存储和查询模块
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -65,4 +65,5 @@ media/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
logs/
|
logs/
|
||||||
packages/
|
packages/
|
||||||
|
*.db
|
||||||
Binary file not shown.
11
apis/log.py
11
apis/log.py
@@ -1,8 +1,10 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from config.database import get_db
|
from config.database import get_db
|
||||||
from schemas.log_alert import LogAlertTrigger
|
from schemas.log_alert import LogAlertTrigger, LogAlertResponse
|
||||||
from service import log_alert_service
|
from service import log_alert_service
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
@@ -12,11 +14,16 @@ router = APIRouter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("", summary="查询日志告警")
|
@router.get("", summary="查询所有日志告警")
|
||||||
def query_alert(db: Session = Depends(get_db)):
|
def query_alert(db: Session = Depends(get_db)):
|
||||||
return log_alert_service.query_alert(db)
|
return log_alert_service.query_alert(db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/name", summary="查询指定日志告警", response_model=List[LogAlertResponse])
|
||||||
|
def query_alert(name: str, db: Session = Depends(get_db)):
|
||||||
|
return log_alert_service.query_alert_by_name(name, db)
|
||||||
|
|
||||||
|
|
||||||
@router.post("", summary="触发日志告警")
|
@router.post("", summary="触发日志告警")
|
||||||
def trigger_log_alter(log_alter: LogAlertTrigger, db: Session = Depends(get_db)) -> bool:
|
def trigger_log_alter(log_alter: LogAlertTrigger, db: Session = Depends(get_db)) -> bool:
|
||||||
return log_alert_service.trigger_alert(log_alter, db)
|
return log_alert_service.trigger_alert(log_alter, db)
|
||||||
|
|||||||
Binary file not shown.
@@ -1,5 +1,4 @@
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from config.setting import settings
|
from config.setting import settings
|
||||||
|
|||||||
32
config/logging.py
Normal file
32
config/logging.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from config.setting import settings
|
||||||
|
|
||||||
|
# 日志级别
|
||||||
|
LOG_LEVEL = settings.log_level.upper()
|
||||||
|
|
||||||
|
# 日志格式
|
||||||
|
STDOUT_FORMAT = (
|
||||||
|
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
||||||
|
"<level>{level: <8}</level> | "
|
||||||
|
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
||||||
|
"<level>{message}</level>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 移除默认处理器
|
||||||
|
logger.remove()
|
||||||
|
|
||||||
|
# 添加控制台处理器
|
||||||
|
logger.add(
|
||||||
|
sink=sys.stdout,
|
||||||
|
level=LOG_LEVEL,
|
||||||
|
format=STDOUT_FORMAT,
|
||||||
|
colorize=True,
|
||||||
|
backtrace=True, # 显示完整异常堆栈
|
||||||
|
diagnose=True, # 显示详细异常信息
|
||||||
|
)
|
||||||
|
|
||||||
|
# 导出配置好的logger
|
||||||
|
__all__ = ["logger"]
|
||||||
Binary file not shown.
@@ -10,12 +10,13 @@ class LogAlert(Base):
|
|||||||
__tablename__ = "log_alerts"
|
__tablename__ = "log_alerts"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
alter_name = Column(String(50))
|
alter_name = Column(String(50), nullable=True)
|
||||||
alter_timestamp = Column(Integer)
|
alter_time = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
timestamp = Column(Integer)
|
log_time = Column(DateTime, nullable=True)
|
||||||
message = Column(Text)
|
log_topic = Column(String(50))
|
||||||
status = Column(String(20), default="pending") # pending, notified, resolved
|
log_message = Column(Text)
|
||||||
|
log_status = Column(String(20), default="pending") # pending, notified, resolved
|
||||||
|
|
||||||
create_time = Column(DateTime, nullable=True, default=datetime.now)
|
create_time = Column(DateTime, nullable=True, default=datetime.now)
|
||||||
update_time = Column(DateTime, nullable=True, default=datetime.now, onupdate=datetime.now)
|
update_time = Column(DateTime, nullable=True, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
fastapi~=0.119.0
|
fastapi~=0.119.0
|
||||||
Binary file not shown.
@@ -1,3 +1,5 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
@@ -6,6 +8,21 @@ class LogAlertTrigger(BaseModel):
|
|||||||
timestamp: int
|
timestamp: int
|
||||||
|
|
||||||
|
|
||||||
|
class LogAlertResponse(BaseModel):
|
||||||
|
alterName: str
|
||||||
|
alterTime: datetime
|
||||||
|
logTime: datetime
|
||||||
|
logTopic: str
|
||||||
|
logMessage: str
|
||||||
|
logStatus: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
json_encoders = {
|
||||||
|
# 自定义 datetime 类型的序列化格式
|
||||||
|
datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
}
|
||||||
|
|
||||||
class OpenobserveQuery(BaseModel):
|
class OpenobserveQuery(BaseModel):
|
||||||
start_time: int
|
start_time: int
|
||||||
end_time: int
|
end_time: int
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,16 +1,19 @@
|
|||||||
from collections import defaultdict
|
from typing import List
|
||||||
|
|
||||||
from sqlalchemy import insert, select
|
from sqlalchemy import insert, select, func
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from models.log_alert import LogAlert
|
from models.log_alert import LogAlert
|
||||||
from schemas.log_alert import LogAlertTrigger
|
from schemas.log_alert import LogAlertTrigger, LogAlertResponse
|
||||||
from service import openobserve_service
|
from service import openobserve_service
|
||||||
from service.message_service import send_wechat_message, MessageEnum
|
from service.message_service import send_wechat_message, MessageEnum
|
||||||
from utils.time import get_timestamp_range, format_timestamp
|
from utils.time import get_timestamp_range, format_timestamp
|
||||||
|
from config.logging import logger
|
||||||
|
|
||||||
|
|
||||||
def trigger_alert(log_alter: LogAlertTrigger, db: Session) -> bool:
|
def trigger_alert(log_alter: LogAlertTrigger, db: Session) -> bool:
|
||||||
|
logger.info(f"{format_timestamp(log_alter.timestamp)} 发生告警:{log_alter.alter_name}")
|
||||||
|
|
||||||
start_time, end_time = get_timestamp_range(log_alter.timestamp, 60)
|
start_time, end_time = get_timestamp_range(log_alter.timestamp, 60)
|
||||||
result = openobserve_service.get_log(start_time, end_time)
|
result = openobserve_service.get_log(start_time, end_time)
|
||||||
hits = result.get("hits")
|
hits = result.get("hits")
|
||||||
@@ -20,20 +23,19 @@ def trigger_alert(log_alter: LogAlertTrigger, db: Session) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# 判断日志告警是否已经存在
|
# 判断日志告警是否已经存在
|
||||||
log_db_alter = db.execute(
|
stmt = select(LogAlert).where(LogAlert.alter_time == format_timestamp(log_alter.timestamp))
|
||||||
select(LogAlert)
|
log_db_alter = db.execute(stmt).first()
|
||||||
.where(LogAlert.alter_timestamp == log_alter.timestamp)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if log_db_alter is not None:
|
if log_db_alter is not None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 企业微信通知
|
|
||||||
alter_message = f"🔔 **告警服务**:{log_alter.alter_name} \n 🕒 **告警时间**:{format_timestamp(log_alter.timestamp)}"
|
|
||||||
send_wechat_message(MessageEnum.MARKDOWN2, alter_message)
|
|
||||||
|
|
||||||
# 批量插入
|
# 批量插入
|
||||||
return batch_insert(log_alter, hits, db)
|
batch_insert(log_alter, hits, db)
|
||||||
|
|
||||||
|
# 通知消息
|
||||||
|
# notify_message(log_alter)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def batch_insert(log_alter: LogAlertTrigger, hits: [], db: Session) -> bool:
|
def batch_insert(log_alter: LogAlertTrigger, hits: [], db: Session) -> bool:
|
||||||
@@ -41,10 +43,11 @@ def batch_insert(log_alter: LogAlertTrigger, hits: [], db: Session) -> bool:
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"alter_name": log_alter.alter_name,
|
"alter_name": log_alter.alter_name,
|
||||||
"alter_timestamp": log_alter.timestamp,
|
"alter_time": format_timestamp(log_alter.timestamp),
|
||||||
"timestamp": hit["_timestamp"],
|
"log_time": format_timestamp(hit["_timestamp"]),
|
||||||
"message": hit["message"],
|
"log_topic": hit["topic"],
|
||||||
"status": "pending"
|
"log_message": hit["message"],
|
||||||
|
"log_status": "pending"
|
||||||
}
|
}
|
||||||
for hit in hits
|
for hit in hits
|
||||||
]
|
]
|
||||||
@@ -56,15 +59,44 @@ def batch_insert(log_alter: LogAlertTrigger, hits: [], db: Session) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def notify_message(log_alter: LogAlertTrigger):
|
||||||
|
# 企业微信通知
|
||||||
|
alter_message = f"🔔 **告警服务**:{log_alter.alter_name} \n 🕒 **告警时间**:{format_timestamp(log_alter.timestamp)}"
|
||||||
|
send_wechat_message(MessageEnum.MARKDOWN2, alter_message)
|
||||||
|
|
||||||
|
|
||||||
def query_alert(db: Session):
|
def query_alert(db: Session):
|
||||||
alerts = db.execute(select(LogAlert)).scalars().all()
|
stmt = (
|
||||||
|
select(
|
||||||
|
LogAlert.alter_name.label("alterName"),
|
||||||
|
func.strftime("%Y-%m-%d %H:%M:%S", LogAlert.alter_time).label("alterTime"),
|
||||||
|
func.strftime("%Y-%m-%d %H:%M:%S", LogAlert.log_time).label("logTime"),
|
||||||
|
LogAlert.log_topic.label("logTopic"),
|
||||||
|
LogAlert.log_message.label("logMessage"),
|
||||||
|
LogAlert.log_status.label("logStatus")
|
||||||
|
)
|
||||||
|
.order_by(LogAlert.alter_name.desc(), LogAlert.alter_time.desc())
|
||||||
|
)
|
||||||
|
|
||||||
# 按照name和timestamp分类
|
results = db.execute(stmt).fetchall()
|
||||||
classified = defaultdict(lambda: defaultdict(list))
|
|
||||||
for alert in alerts:
|
|
||||||
classified[alert.alter_name][alert.alter_timestamp].append({
|
|
||||||
"timestamp": format_timestamp(alert.timestamp),
|
|
||||||
"message": alert.message
|
|
||||||
})
|
|
||||||
|
|
||||||
return dict(classified)
|
return [LogAlertResponse.model_validate(result) for result in results]
|
||||||
|
|
||||||
|
|
||||||
|
def query_alert_by_name(name: str, db: Session) -> List[LogAlertResponse]:
|
||||||
|
stmt = (
|
||||||
|
select(
|
||||||
|
LogAlert.alter_name.label("alterName"),
|
||||||
|
func.strftime("%Y-%m-%d %H:%M:%S", LogAlert.alter_time).label("alterTime"),
|
||||||
|
func.strftime("%Y-%m-%d %H:%M:%S", LogAlert.log_time).label("logTime"),
|
||||||
|
LogAlert.log_topic.label("logTopic"),
|
||||||
|
LogAlert.log_message.label("logMessage"),
|
||||||
|
LogAlert.log_status.label("logStatus")
|
||||||
|
)
|
||||||
|
.where(LogAlert.alter_name == name)
|
||||||
|
.order_by(LogAlert.alter_time.desc(), LogAlert.log_time.asc())
|
||||||
|
)
|
||||||
|
|
||||||
|
results = db.execute(stmt).fetchall()
|
||||||
|
|
||||||
|
return [LogAlertResponse.model_validate(result) for result in results]
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ def send_wechat_message(message_type: MessageEnum, message: str):
|
|||||||
settings.wechat_webhook_url,
|
settings.wechat_webhook_url,
|
||||||
headers={"Content-Type": "application/json"},
|
headers={"Content-Type": "application/json"},
|
||||||
params={'key': settings.wechat_webhook_key},
|
params={'key': settings.wechat_webhook_key},
|
||||||
data=json.dumps(wechat_message, ensure_ascii=False).encode('utf-8')
|
data=json.dumps(wechat_message, ensure_ascii=False).encode('utf-8'),
|
||||||
|
timeout=5
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
Binary file not shown.
@@ -21,5 +21,5 @@ def get_timestamp_range(ts: int, delta_seconds: int = 5, unit: str = "microsecon
|
|||||||
return ts - delta, ts + delta
|
return ts - delta, ts + delta
|
||||||
|
|
||||||
|
|
||||||
def format_timestamp(timestamp: int) -> str:
|
def format_timestamp(timestamp: int) -> datetime:
|
||||||
return datetime.fromtimestamp(timestamp / 1_000_000).strftime("%Y-%m-%d %H:%M:%S")
|
return datetime.fromtimestamp(timestamp / 1_000_000)
|
||||||
|
|||||||
Reference in New Issue
Block a user