feat:初始化工程

This commit is contained in:
2025-10-19 22:16:27 +08:00
commit cb1a26b270
41 changed files with 657 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

10
.idea/log-alert-service.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (log-alert-service)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (log-alert-service)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/log-alert-service.iml" filepath="$PROJECT_DIR$/.idea/log-alert-service.iml" />
</modules>
</component>
</project>

Binary file not shown.

6
apis/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
from fastapi import FastAPI
from .log import router as log_router
def register_routers(app: FastAPI):
app.include_router(log_router, prefix="")

Binary file not shown.

Binary file not shown.

15
apis/log.py Normal file
View File

@@ -0,0 +1,15 @@
from fastapi import APIRouter
from schemas.log_alert import LogAlertTrigger
from service import log_alert_service
router = APIRouter(
prefix="/log-alter",
tags=["日志告警"],
responses={404: {"description": "Not found"}}
)
@router.post("", summary="触发日志告警")
def trigger_log_alter(log_alter: LogAlertTrigger):
return log_alert_service.trigger_alert(log_alter)

Binary file not shown.

Binary file not shown.

26
config/database.py Normal file
View File

@@ -0,0 +1,26 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from config.setting import settings
from models.log_alert import Base
engine = create_engine(
settings.database_url, connect_args={"check_same_thread": False}
)
# 会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def init_db():
Base.metadata.create_all(bind=engine)
def get_db():
# 创建数据库会话实例
db = SessionLocal()
try:
yield db
finally:
db.close()

18
config/setting.py Normal file
View File

@@ -0,0 +1,18 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
# sqlite连接方式sqlite:///
database_url: str = "sqlite:///./logs.db"
wechat_webhook: str = ""
smtp_server: str = ""
smtp_port: int = 587
smtp_user: str = ""
smtp_password: str = ""
smtp_sender: str = "log-alert@example.com"
class Config:
env_file = ".env"
settings = Settings()

0
id_generator/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

38
id_generator/generator.py Normal file
View File

@@ -0,0 +1,38 @@
"""
雪花算法生成器IdGenerator
"""
# !/usr/bin/python
# coding=UTF-8
from . import options
from . import snowflake_m1
class DefaultIdGenerator:
"""
ID生成器
"""
def __init__(self):
self.snowflake = None
def set_id_generator(self, option: options.IdGeneratorOptions):
"""
设置id生成规则信息
"""
if option.base_time < 100000:
raise ValueError("base time error.")
self.snowflake = snowflake_m1.SnowFlakeM1(option)
def next_id(self) -> int:
"""
获取新的UUID
"""
if self.snowflake is None:
raise ValueError("please set id generator at first.")
return self.snowflake.next_id()

134
id_generator/idregister.py Normal file
View File

@@ -0,0 +1,134 @@
"""
worker id generator
"""
# !/usr/bin/python
# coding=UTF-8
from threading import Thread
import time
import logging
import redis
class Register:
"""
redis封装
- host 代表redis ip
- port 代表redis端口
- max_worker_id worker_id的最大值, 默认为100
- password redis的密码, 默认为空
"""
def __init__(self, host, port, max_worker_id=100, password=None):
self.redis_impl = redis.StrictRedis(host=host, port=port, db=0, password=password)
self.loop_count = 0
self.max_loop_count = 10
self.worker_id_expire_time = 15
self.max_worker_id = max_worker_id
self.worker_id = -1
self.is_stop = False
def get_lock(self, key):
"""
获取分布式全局锁,并设置过期时间为30秒
"""
if self.redis_impl.setnx(key, 1):
self.redis_impl.expire(key, 30)
return True
if self.redis_impl.ttl(key) < 0:
self.redis_impl.expire(key, 30)
return False
def stop(self):
"""
退出注册器的线程
"""
self.is_stop = True
def get_worker_id(self):
"""
获取全局唯一worker_id, 会创建一个线程给worker id续期
失败返回-1
"""
self.loop_count = 0
def extern_life(my_id):
while 1:
time.sleep(self.worker_id_expire_time / 3)
# 是否关闭了
if self.is_stop:
return
# 更新生命周期
if self.worker_id != my_id:
break
try:
self.redis_impl.expire(
f"IdGen:WorkerId:Value:{my_id}",
self.worker_id_expire_time)
except Exception as exe:
logging.error(exe)
continue
self.worker_id = self.__get_next_worker_id()
if self.worker_id > -1:
Thread(target=extern_life, args=[self.worker_id]).start()
return self.worker_id
def __get_next_worker_id(self):
"""
获取全局唯一worker id内部实现
"""
cur = self.redis_impl.incrby("IdGen:WorkerId:Index", 1)
def can_reset():
try:
reset_value = self.redis_impl.incr("IdGen:WorkerId:Value:Edit")
return reset_value != 1
except Exception as ept:
logging.error(ept)
return False
def end_reset():
try:
self.redis_impl.set("IdGen:WorkerId:Value:Edit", 0)
except Exception as ept:
logging.error(ept)
def is_available(worker_id: int):
try:
rst = self.redis_impl.get(f"IdGen:WorkerId:Value:{worker_id}")
return rst != "Y"
except Exception as ept:
logging.error(ept)
return False
if cur > self.max_worker_id:
if can_reset():
self.redis_impl.set("IdGen:WorkerId:Index", -1)
end_reset()
self.loop_count += 1
if self.loop_count > self.max_loop_count:
self.loop_count = 0
return -1
time.sleep(0.2 * self.loop_count)
return self.__get_next_worker_id()
time.sleep(0.2)
return self.__get_next_worker_id()
if is_available(cur):
self.redis_impl.setex(
f"IdGen:WorkerId:Value:{cur}",
self.worker_id_expire_time,
"Y"
)
self.loop_count = 0
return cur
return self.__get_next_worker_id()

43
id_generator/options.py Normal file
View File

@@ -0,0 +1,43 @@
"""
生成器IdGenerator配置选项
"""
# !/usr/bin/python
# coding=UTF-8
class IdGeneratorOptions:
"""
ID生成器配置
- worker_id 全局唯一id, 区分不同uuid生成器实例
- worker_id_bit_length 生成的uuid中worker_id占用的位数
- seq_bit_length 生成的uuid中序列号占用的位数
"""
def __init__(self, worker_id=0, worker_id_bit_length=6, seq_bit_length=6):
# 雪花计算方法,1-漂移算法|2-传统算法), 默认1。目前只实现了1。
self.method = 1
# 基础时间ms单位, 不能超过当前系统时间
self.base_time = 1582136402000
# 机器码, 必须由外部设定, 最大值 2^worker_id_bit_length-1
self.worker_id = worker_id
# 机器码位长, 默认值6, 取值范围 [1, 15](要求:序列数位长+机器码位长不超过22
self.worker_id_bit_length = worker_id_bit_length
# 序列数位长, 默认值6, 取值范围 [3, 21](要求:序列数位长+机器码位长不超过22
self.seq_bit_length = seq_bit_length
# 最大序列数(含), 设置范围 [max_seq_number, 2^seq_bit_length-1]
# 默认值0, 表示最大序列数取最大值2^seq_bit_length-1]
self.max_seq_number = 0
# 最小序列数(含), 默认值5, 取值范围 [5, max_seq_number], 每毫秒的前5个序列数对应编号0-4是保留位
# 其中1-4是时间回拨相应预留位, 0是手工新值预留位
self.min_seq_number = 5
# 最大漂移次数(含), 默认2000, 推荐范围500-10000与计算能力有关
self.top_over_cost_count = 2000

20
id_generator/snowflake.py Normal file
View File

@@ -0,0 +1,20 @@
"""
雪花算法生成器接口声明
"""
# !/usr/bin/python
# coding=UTF-8
class SnowFlake():
def __init__(self, options):
self.options = options
def next_id(self) -> int:
"""
获取新的UUID
"""
return 0

View File

@@ -0,0 +1,147 @@
"""
M1生成器
"""
# !/usr/bin/python
# coding=UTF-8
import threading
import time
from .snowflake import SnowFlake
from .options import IdGeneratorOptions
class SnowFlakeM1(SnowFlake):
"""
M1规则ID生成器配置
"""
def __init__(self, options: IdGeneratorOptions):
# 1.base_time
self.base_time = 1582136402000
if options.base_time != 0:
self.base_time = int(options.base_time)
# 2.worker_id_bit_length
self.worker_id_bit_length = 6
if options.worker_id_bit_length != 0:
self.worker_id_bit_length = int(options.worker_id_bit_length)
# 3.worker_id
self.worker_id = options.worker_id
# 4.seq_bit_length
self.seq_bit_length = 6
if options.seq_bit_length != 0:
self.seq_bit_length = int(options.seq_bit_length)
# 5.max_seq_number
self.max_seq_number = int(options.max_seq_number)
if options.max_seq_number <= 0:
self.max_seq_number = (1 << self.seq_bit_length) - 1
# 6.min_seq_number
self.min_seq_number = int(options.min_seq_number)
# 7.top_over_cost_count
self.top_over_cost_count = int(options.top_over_cost_count)
# 8.Others
self.__timestamp_shift = self.worker_id_bit_length + self.seq_bit_length
self.__current_seq_number = self.min_seq_number
self.__last_time_tick: int = 0
self.__turn_back_time_tick: int = 0
self.__turn_back_index: int = 0
self.__is_over_cost = False
self.___over_cost_count_in_one_term: int = 0
self.__id_lock = threading.Lock()
def __next_over_cost_id(self) -> int:
current_time_tick = self.__get_current_time_tick()
if current_time_tick > self.__last_time_tick:
self.__last_time_tick = current_time_tick
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = False
self.___over_cost_count_in_one_term = 0
return self.__calc_id(self.__last_time_tick)
if self.___over_cost_count_in_one_term >= self.top_over_cost_count:
self.__last_time_tick = self.__get_next_time_tick()
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = False
self.___over_cost_count_in_one_term = 0
return self.__calc_id(self.__last_time_tick)
if self.__current_seq_number > self.max_seq_number:
self.__last_time_tick += 1
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = True
self.___over_cost_count_in_one_term += 1
return self.__calc_id(self.__last_time_tick)
return self.__calc_id(self.__last_time_tick)
def __next_normal_id(self) -> int:
current_time_tick = self.__get_current_time_tick()
if current_time_tick < self.__last_time_tick:
if self.__turn_back_time_tick < 1:
self.__turn_back_time_tick = self.__last_time_tick - 1
self.__turn_back_index += 1
# 每毫秒序列数的前5位是预留位, 0用于手工新值, 1-4是时间回拨次序
# 支持4次回拨次序避免回拨重叠导致ID重复, 可无限次回拨(次序循环使用)。
if self.__turn_back_index > 4:
self.__turn_back_index = 1
return self.__calc_turn_back_id(self.__turn_back_time_tick)
# 时间追平时, _TurnBackTimeTick清零
self.__turn_back_time_tick = min(self.__turn_back_time_tick, 0)
if current_time_tick > self.__last_time_tick:
self.__last_time_tick = current_time_tick
self.__current_seq_number = self.min_seq_number
return self.__calc_id(self.__last_time_tick)
if self.__current_seq_number > self.max_seq_number:
self.__last_time_tick += 1
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = True
self.___over_cost_count_in_one_term = 1
return self.__calc_id(self.__last_time_tick)
return self.__calc_id(self.__last_time_tick)
def __calc_id(self, use_time_tick) -> int:
self.__current_seq_number += 1
return (
(use_time_tick << self.__timestamp_shift) +
(self.worker_id << self.seq_bit_length) +
self.__current_seq_number
) % int(1e64)
def __calc_turn_back_id(self, use_time_tick) -> int:
self.__turn_back_time_tick -= 1
return (
(use_time_tick << self.__timestamp_shift) +
(self.worker_id << self.seq_bit_length) +
self.__turn_back_index
) % int(1e64)
def __get_current_time_tick(self) -> int:
return int((time.time_ns() / 1e6) - self.base_time)
def __get_next_time_tick(self) -> int:
temp_time_ticker = self.__get_current_time_tick()
while temp_time_ticker <= self.__last_time_tick:
# 0.001 = 1 mili sec
time.sleep(0.001)
temp_time_ticker = self.__get_current_time_tick()
return temp_time_ticker
def next_id(self) -> int:
with self.__id_lock:
if self.__is_over_cost:
nextid = self.__next_over_cost_id()
else:
nextid = self.__next_normal_id()
return nextid

BIN
logs.db Normal file

Binary file not shown.

14
main.py Normal file
View File

@@ -0,0 +1,14 @@
from fastapi import FastAPI
from apis import register_routers
from config.database import init_db
app = FastAPI(title="Log Alert Service")
@app.on_event("startup")
def on_startup():
init_db()
register_routers(app)

Binary file not shown.

Binary file not shown.

40
models/base.py Normal file
View File

@@ -0,0 +1,40 @@
from sqlalchemy import Column, BigInteger, DateTime, event
from sqlalchemy.ext.declarative import declared_attr, declarative_base
from datetime import datetime
from config.database import Base
from utils.common import camel_to_snake
from id_generator import options, generator
# https://github.com/yitter/IdGenerator/tree/master/Python
options = options.IdGeneratorOptions(worker_id=23)
idgen = generator.DefaultIdGenerator()
idgen.set_id_generator(options)
# 第二层基类包含ID
class IdBase(Base):
__abstract__ = True
id = Column(BigInteger, primary_key=True, index=True)
@declared_attr
def __tablename__(cls):
# 自动把数据库实体类名驼峰转为数据库表名下划线
return camel_to_snake(cls.__name__)
# 自动填充id
@event.listens_for(IdBase, 'before_insert', propagate=True)
def before_insert_listener(mapper, connection, target):
if target.id is None:
target.id = idgen.next_id()
# 第二层基类包含ID和审计字段
class AuditBase(IdBase):
__abstract__ = True
create_time = Column(DateTime, nullable=True, default=datetime.now)
update_time = Column(DateTime, nullable=True, default=datetime.now, onupdate=datetime.now)

17
models/log_alert.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, DateTime, Text
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class LogAlert(Base):
__tablename__ = "log_alerts"
id = Column(Integer, primary_key=True, index=True)
timestamp = Column(DateTime)
log_level = Column(String(20))
message = Column(Text)
source = Column(String(100))
context = Column(Text) # 存储JSON格式的上下文信息
status = Column(String(20), default="pending") # pending, notified, resolved

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
fastapi~=0.119.0
pydantic~=2.12.2
SQLAlchemy~=2.0.44
requests~=2.32.5

Binary file not shown.

14
schemas/log_alert.py Normal file
View File

@@ -0,0 +1,14 @@
from pydantic import BaseModel, Field
class LogAlertTrigger(BaseModel):
alter_name: str
timestamp: int
class OpenobserveQuery(BaseModel):
start_time: int
end_time: int
from_: int = Field(default=0, alias="from")
size: int = 10
sql: str = "select * from default"

Binary file not shown.

View File

@@ -0,0 +1,31 @@
import requests
from requests.auth import HTTPBasicAuth
from schemas.log_alert import LogAlertTrigger, OpenobserveQuery
from service import openobserve_service
def trigger_alert(log_alter: LogAlertTrigger):
#print(log_alter)
# openobserve_service.get_log()
url = 'http://192.168.1.7:5080/api/default/_search'
username = 'njcxx0822@163.com'
password = '19940822Cxx'
response = requests.post(
url,
headers={
'accept': 'application/json',
'Content-Type': 'application/json'
},
json={
"query": OpenobserveQuery(
start_time=1760773184953420,
end_time=1760773189953431
).model_dump()
},
auth=HTTPBasicAuth(username, password)
)
return response.json().get("hits")

View File

@@ -0,0 +1,27 @@
import requests
from requests.auth import HTTPBasicAuth
from schemas.log_alert import OpenobserveQuery
def get_log():
url = 'http://192.168.1.7:5080/api/default/_search'
username = 'njcxx0822@163.com'
password = '19940822Cxx'
response = requests.post(
url,
headers={
'accept': 'application/json',
'Content-Type': 'application/json'
},
json={
"query": OpenobserveQuery(
start_time=1760773184953420,
end_time=1760773189953431
).model_dump()
},
auth=HTTPBasicAuth(username, password)
)
print(response)

11
test_main.http Normal file
View File

@@ -0,0 +1,11 @@
# Test your FastAPI endpoints
GET http://127.0.0.1:8000/
Accept: application/json
###
GET http://127.0.0.1:8000/hello/User
Accept: application/json
###

Binary file not shown.

13
utils/common.py Normal file
View File

@@ -0,0 +1,13 @@
import re
def camel_to_snake(name: str) -> str:
"""将驼峰命名转换为蛇形命名CamelCase → snake_case"""
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
def snake_to_camel(name: str) -> str:
"""将蛇形命名转换为驼峰命名snake_case → CamelCase"""
components = name.split('_')
return ''.join(x.title() for x in components)