Files
blog-press/docs/Web-Backend/FastAPI/FastAPI-Guide.md
2025-12-15 19:53:21 +08:00

27 KiB
Raw Blame History

一、简介

FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 并基于标准的 Python 类型提示。
  安装:

pip install fastapi
pip install uvicorn[standard]  # ASGI服务器

二、数据层

2.1 数据库ORM SQLAlchemy

2.1.1 配置与连接

# database.py - 数据库配置
from sqlalchemy import create_engine, MetaData
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from typing import Generator
import os
from dotenv import load_dotenv

load_dotenv()

# 数据库配置
DATABASE_URL = f"mysql+pymysql://{os.getenv("DATABASE_URL", "root:123456@127.0.0.1:3306/test")}"

# 创建引擎
engine = create_engine(
    DATABASE_URL,
    echo=True,  # 显示 SQL 语句(开发环境)
    pool_size=20,  # 连接池大小
    max_overflow=40,  # 最大溢出连接数
    pool_pre_ping=True,  # 连接前 ping
    pool_recycle=3600,  # 连接回收时间(秒)
)

# 创建会话工厂
SessionLocal = sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=engine,
    expire_on_commit=False,  # 提交后不使实例过期
)

# 声明基类
Base = declarative_base()

# 依赖注入:获取数据库会话
def get_db() -> Generator[Session, None, None]:
    """
    获取数据库会话
    使用 yield 确保会话正确关闭
    """
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

::: warning 需要把pool_pre_ping=True打开否则会出现超过pool_recycle时间后数据库断线。 :::

2.1.2 数据库基类

from sqlalchemy import Column, BigInteger, String, DateTime, event
from sqlalchemy.ext.declarative import declared_attr

from config.auth import context_sub
from config.database import Base
from datetime import datetime

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)
    create_by = Column(String(255), nullable=True)
    update_time = Column(DateTime, nullable=True, default=datetime.now, onupdate=datetime.now)
    update_by = Column(String(255), nullable=True)


@event.listens_for(AuditBase, 'before_insert', propagate=True)
def before_insert(mapper, connection, target):
    value = context_sub.get(None)
    if value is not None:
        target.create_by = value


@event.listens_for(AuditBase, 'before_update', propagate=True)
def before_update(mapper, connection, target):
    value = context_sub.get(None)
    if value is not None:
        target.update_by = value

id采用yitter雪花id。
审计字段中的create_time和update_time通过SQLAlchemy参数配置。
create_by和update_by字段为登录认证中存储的账号信息。

2.1.3 数据库模型

from sqlalchemy import (BigInteger, Boolean, Column, Integer, DECIMAL, String, LargeBinary)
from sqlalchemy.orm import relationship

from models.base import AuditBase, IdBase


class Blog(AuditBase):
    title = Column(String(255), nullable=False, comment="博客标题")
    top_value = Column(Integer, nullable=False, default=0, comment="置顶值 越大越靠前")
    is_great = Column(Boolean, nullable=False, default=False, comment="是否是精品")
    category_id = Column(BigInteger, nullable=False, comment="博客类别")
    summary = Column(String(255), nullable=False, comment="博客内容概要")
    content_id = Column(BigInteger, nullable=False, comment="博客内容")
    word_count = Column(Integer, nullable=False, default=0, comment="字数统计")
    read_duration = Column(DECIMAL(10, 2), nullable=False, default=0.00, comment="阅读时长")
    is_approved = Column(Boolean, nullable=False, default=False, comment="是否发布")

    # 分类关系(多对一)
    category = relationship(
        "BlogCategory",
        # 与BlogCategory的blogs属性建立双向关系
        back_populates="blogs",
        # 只级联保存和合并操作,不级联删除(删除博客不应删除分类
        cascade="save-update, merge",
        # 明确指定连接条件
        # 如果数据库设置了外键可以省略
        primaryjoin="foreign(Blog.category_id) == BlogCategory.id"
    )

    # 内容关系(一对一)
    content = relationship(
        "BlogContent",
        # 与BlogContent的blog属性建立双向关系
        back_populates="blog",
        # 完全级联操作:保存、合并、刷新、删除等所有操作都会级联
        cascade="all, delete-orphan",
        # 设置为False表示一对一关系返回单个对象而不是列表
        uselist=False,
        # 确保内容只有一个父博客与delete-orphan配合使用
        single_parent=True,
        # 明确指定连接条件
        primaryjoin="foreign(Blog.content_id) == BlogContent.id"
    )

    # 评论(一对多)
    comments = relationship(
        "BlogComment",
        # 与BlogComment的blog属性建立双向关系
        back_populates="blog",
        # 完全级联操作:博客删除时自动删除所有评论
        cascade="all, delete-orphan",
        # 明确指定连接条件
        primaryjoin="Blog.id == foreign(BlogComment.blog_id)"
    )


class BlogCategory(AuditBase):
    name = Column(String(45), nullable=False, comment="类别名称")

    blogs = relationship(
        "Blog",
        # 与Blog的category属性建立双向关系
        back_populates="category",
        # 完全级联操作:分类删除时自动删除所有关联的博客
        # 警告:这会级联删除分类下的所有博客,包括博客的内容、访问记录和评论
        cascade="all, delete-orphan",
        # 明确指定连接条件
        primaryjoin="BlogCategory.id == foreign(Blog.category_id)"
    )


class BlogContent(IdBase):
    content = Column(LargeBinary, nullable=False, comment="博客内容")

    blog = relationship(
        "Blog",
        # 与Blog的content属性建立双向关系
        back_populates="content",
        # 设置为False表示一对一关系
        uselist=False,
        # 明确指定连接条件
        primaryjoin="BlogContent.id == foreign(Blog.content_id)"
    )


class BlogComment(AuditBase):
    blog_id = Column(BigInteger, nullable=False, comment="博客ID")
    parent_id = Column(BigInteger, nullable=False, comment="父评论ID")
    name = Column(String(255), nullable=False, comment="评论人昵称")
    website = Column(String(255), nullable=True, comment="评论人网站")
    ip_address = Column(String(45), nullable=False, comment="评论人IP")
    user_agent = Column(String(255), nullable=False, comment="评论人浏览器信息")
    content = Column(String(255), nullable=False, comment="评论内容")
    is_approved = Column(Boolean, nullable=False, default=False, comment="是否通过")

    blog = relationship(
        "Blog",
        # 与Blog的comments属性建立双向关系
        back_populates="comments",
        # 明确指定连接条件
        primaryjoin="foreign(BlogComment.blog_id) == Blog.id"
    )

这里没有通过在数据库建立外键而是通过SQLAlchemy中的relationship来建立。
其中primaryjoin明确指定了连接条件。如果为一对一需要设置uselist=False和single_parent=True返回单个对象而不是列表。
cascade表示级联操作

# cascade 的完整可选值列表:

# 基本级联操作
"save-update"      # 保存/更新时级联
"merge"           # 合并会话时级联
"refresh-expire"  # 刷新过期对象时级联
"expunge"         # 从会话中移除时级联
"delete"          # 删除时级联
"delete-orphan"   # 成为孤儿时删除

# 快捷组合
"all"                     # 包含除 delete-orphan 外的所有操作
"all, delete-orphan"      # 包含所有操作
"none"                    # 禁用所有级联(默认)

# 其他组合
"save-update, merge"               # 常用组合
"save-update, merge, delete"       # 包含删除
"save-update, merge, refresh-expire"

2.2 数据验证pydantic

from typing import Optional

from pydantic import BaseModel, Field, field_validator, ConfigDict
from datetime import datetime


class BlogQuery(BaseModel):
    """博客查询参数"""
    category: Optional[str] = Field(None, description="分类名称")
    title: Optional[str] = Field(None, description="标题关键词")
    year: Optional[int] = Field(None, description="发布年份", ge=2000, le=datetime.now().year)

    @field_validator('year')
    def validate_year(cls, v):
        if v is not None and v > datetime.now().year:
            raise ValueError('年份不能超过当前年份')
        return v


class BlogBase(BaseModel):
    """博客基础模型"""
    title: str = Field(..., min_length=1, max_length=255, description="博客标题")
    top_value: int = Field(default=0, ge=0, description="置顶值,越大越靠前", alias="topValue")
    is_great: bool = Field(default=False, description="是否是精品", alias="isGreat")
    category: str = Field(..., min_length=1, max_length=45, description="分类名称")
    content: Optional[str] = Field(None, description="博客内容")
    is_approved: bool = Field(default=False, description="是否已发布", alias="isApproved")

    @field_validator('title')
    def title_not_empty(cls, v):
        if not v or not v.strip():
            raise ValueError('标题不能为空')
        return v.strip()

    @field_validator('category')
    def category_not_empty(cls, v):
        if not v or not v.strip():
            raise ValueError('分类不能为空')
        return v.strip()


class BlogCreate(BlogBase):
    pass


class BlogUpdate(BlogBase):
    pass


class BlogResponse(BlogBase):
    id: int = Field(..., description="博客ID")
    summary: Optional[str] = Field(None, description="内容摘要")
    word_count: Optional[int] = Field(None, description="字数统计", alias="wordCount")
    read_duration: Optional[float] = Field(None, description="阅读时长", alias="readDuration")
    visit_count: Optional[int] = Field(0, description="访问次数", alias="visitCount")
    create_time: datetime = Field(..., description="创建时间", alias="createTime")
    update_time: datetime = Field(..., description="更新时间", alias="updateTime")

    model_config = ConfigDict(
        from_attributes=True,
        populate_by_name=True,
        json_encoders={
            datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
        }
    )


class BlogCategoryResponse(BaseModel):
    name: str = Field(..., description="分类名称")
    count: int = Field(..., description="博客数量")

    model_config = ConfigDict(
        from_attributes=True,
        populate_by_name=True
    )


class BlogStatsResponse(BaseModel):
    blog_count: int = Field(..., description="博客总数", alias="blogCount")
    category_count: int = Field(..., description="分类总数", alias="categoryCount")
    word_count: int = Field(..., description="总字数", alias="wordCount")


class BlogVisitResponse(BaseModel):
    ip: str = Field(..., description="IP地址")
    os: str = Field(..., description="操作系统")
    browser: str = Field(..., description="浏览器")
    uri: str = Field(..., description="访问路径")
    title: str = Field(None, description="博客标题")
    visit_time: datetime = Field(..., description="访问时间", alias="visitTime")

    model_config = ConfigDict(
        from_attributes=True,
        populate_by_name=True,
        json_encoders={
            datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
        }
    )


class BlogLatestResponse(BaseModel):
    id: int = Field(..., description="博客ID")
    title: str = Field(..., description="博客标题")

    model_config = ConfigDict(
        from_attributes=True,
        populate_by_name=True
    )


class BlogAdjacentResponse(BaseModel):
    id: int = Field(..., description="博客ID")
    title: str = Field(..., description="博客标题")

    model_config = ConfigDict(
        from_attributes=True,
        populate_by_name=True
    )


class BlogCommentCreate(BaseModel):
    parent_id: int = Field(default=0, ge=0, description="父评论ID0表示顶级评论", alias="parentId")
    name: str = Field(..., min_length=1, max_length=50, description="评论人昵称")
    website: Optional[str] = Field(None, description="评论人网站")
    content: str = Field(..., min_length=1, max_length=1000, description="评论内容")

    @field_validator('name')
    def name_not_empty(cls, v):
        if not v or not v.strip():
            raise ValueError('昵称不能为空')
        return v.strip()

    @field_validator('content')
    def content_not_empty(cls, v):
        if not v or not v.strip():
            raise ValueError('评论内容不能为空')
        return v.strip()


class BlogCommentResponse(BlogCommentCreate):
    id: int = Field(..., description="评论ID")
    ip_address: str = Field(..., description="IP地址", alias="ipAddress")
    user_agent: str = Field(..., description="浏览器信息", alias="userAgent")
    create_time: datetime = Field(..., description="创建时间", alias="createTime")

    model_config = ConfigDict(
        from_attributes=True,
        populate_by_name=True,
        json_encoders={
            datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
        }
    )

@field_validator为Pydantic V2的验证器装饰器。
ConfigDict为全局配置类,from_attributes=True表示允许从SQLAlchemy等ORM对象创建。populate_by_name=True表示允许通过字段的别名alias来赋值 json_encoders表示自定义特定类型的 JSON 序列化方式。

三、服务层

3.1 查询操作

def query_blog_by_id(db: Session, blog_id: int) -> BlogResponse:
    check_blog_exist(db, blog_id)

    stmt = (select(
        Blog.id,
        Blog.title,
        Blog.top_value,
        Blog.is_great,
        BlogCategory.name.label("category"),
        Blog.summary,
        BlogContent.content,
        Blog.word_count,
        Blog.read_duration,
        func.count(BlogVisit.id).label("visitCount"),
        Blog.is_approved,
        Blog.create_time,
        Blog.update_time
    ).where(Blog.id == blog_id)
            .outerjoin(BlogCategory, Blog.category_id == BlogCategory.id)
            .outerjoin(BlogContent, Blog.content_id == BlogContent.id)
            .outerjoin(BlogVisit, Blog.id == BlogVisit.blog_id))

    blog = db.execute(stmt).first()

    return BlogResponse.model_validate(blog)

  条件和分页查询

def query_blog_by_condition(db: Session, blog_query: BlogQuery) -> List[BlogResponse]:
    stmt = get_query_blog_by_condition_stmt(blog_query)
    results = db.execute(stmt).fetchall()
    return [BlogResponse.model_validate(result) for result in results]


def get_query_blog_by_condition_stmt(blog_query: BlogQuery):
    stmt = (select(
        Blog.id,
        Blog.title,
        Blog.top_value,
        Blog.is_great,
        BlogCategory.name.label("category"),
        Blog.summary,
        BlogContent.content,
        Blog.word_count,
        Blog.read_duration,
        func.count(BlogVisit.id).label("visitCount"),
        Blog.is_approved,
        Blog.create_time,
        Blog.update_time
    ).where(Blog.is_approved == 1)
            .outerjoin(BlogCategory, Blog.category_id == BlogCategory.id)
            .outerjoin(BlogContent, Blog.content_id == BlogContent.id)
            .outerjoin(BlogVisit, Blog.id == BlogVisit.blog_id)
            .group_by(Blog.id))

    conditions = []

    if blog_query.category:
        conditions.append(BlogCategory.name == blog_query.category)

    if blog_query.title:
        conditions.append(Blog.title.like(f"%{blog_query.title}%"))

    if blog_query.year:
        conditions.append(func.extract('year', Blog.create_time) == blog_query.year)

    if conditions:
        stmt = stmt.where(and_(*conditions))

    stmt = stmt.order_by(desc(Blog.is_great), desc(Blog.update_time))

    return stmt


def query_blog_by_condition_page(db: Session, blog_query: BlogQuery,
                                 current_page: int = 1, page_size: int = 10) -> PageResult[BlogResponse]:
    stmt = get_query_blog_by_condition_stmt(blog_query)
    return paginate_query(db, stmt, current_page, page_size)

def paginate_query(db: Session, query, current_page: int = 1, page_size: int = 10) -> PageResult:
    """通用分页查询函数"""
    # 计算总记录数
    total = db.execute(select(func.count()).select_from(query.subquery())).scalar_one_or_none() or 0

    # 计算总页数
    total_pages = (total + page_size - 1) // page_size if page_size != 0 else 0

    # 执行分页查询
    results = db.execute(query.offset((current_page - 1) * page_size).limit(page_size)).all()

    # 转换为字典列表
    records = [row._asdict() if hasattr(row, "_asdict") else dict(row) for row in results]

    return PageResult(
        current=current_page,
        size=page_size,
        total=total,
        pages=total_pages,
        records=records
    )

3.2 新增操作

def add_blog(db: Session, blog: BlogCreate) -> bool:
    word_count = get_word_count(blog.content)

    db_blog = Blog(
        title=blog.title,
        top_value=blog.top_value,
        is_great=blog.is_great,
        category_id=add_blog_category(db, blog.category),
        summary=get_blog_summary(blog.content),
        word_count=word_count,
        read_duration=get_read_duration(word_count),
        is_approved=blog.is_approved
    )

    # 级联新增
    db_blog.content = BlogContent(content=blog.content.encode('utf-8'))

    db.add(db_blog)
    db.commit()
    db.refresh(db_blog)

    return True

  这里的db_blog.content可以直接赋值BlogContent对象实现级联新增,不用传content_id
::: warning 这里的category和blog是多对一的关系且需要一定的逻辑处理不能直接赋值BlogCategory对象,否则每新增一条博客都会新增一个category。 :::

3.3 更新操作

def update_blog(db: Session, blog_id: int, blog: BlogUpdate) -> bool:
    db_blog = check_blog_exist(db, blog_id)

    db_blog.title = blog.title
    db_blog.top_value = blog.topValue
    db_blog.is_great = blog.isGreat
    db_blog.category_id = update_blog_category(db, blog.category)

    # 级联更新
    db_blog.content = BlogContent(content=blog.content.encode('utf-8'))

    word_count = get_word_count(blog.content)
    db_blog.summary = get_blog_summary(blog.content),
    db_blog.word_count = word_count
    db_blog.read_duration = get_read_duration(word_count)
    db_blog.is_approved = blog.isApproved

    db.commit()
    db.refresh(db_blog)

    return True

3.4 删除操作

def delete_blog(db: Session, blog_id: int) -> bool:
    db_blog = check_blog_exist(db, blog_id)

    # 级联删除
    db.delete(db_blog)
    db.commit()

    return True

四、路由层

router = APIRouter(
    prefix="/blog",
    tags=["博客管理"],
    responses={404: {"description": "Not found"}}
)

@router.get("/page", summary="分页查询博客", response_model=PageResult[BlogResponse])
def query_blog_by_page(
        current_page: int = Query(1, ge=1, alias="currentPage", description="当前页码从1开始"),
        page_size: int = Query(10, ge=1, le=100, alias="pageSize", description="每页显示数量1-100之间"),
        db: Session = Depends(get_db)
):
    return blog_service.query_blog_by_page(db, current_page, page_size)

@router.get("/condition", summary="条件查询博客", response_model=List[BlogResponse])
def query_blog_by_condition(query: BlogQuery = Depends(), db: Session = Depends(get_db)):
    return blog_service.query_blog_by_condition(db, query)

@router.post("", summary="新增博客内容", response_model=bool)
def add_blog(
        blog: BlogCreate = Body(..., description="博客创建数据"),
        db: Session = Depends(get_db),
        _=Depends(verify_token)
):
    return blog_service.add_blog(db, blog)

@router.put("/{blog_id}", summary="更新博客内容", response_model=bool)
def update_blog(
        blog_id: int = Path(..., ge=1, description="博客ID"),
        blog: BlogUpdate = Body(..., description="博客更新数据"),
        db: Session = Depends(get_db),
        _=Depends(verify_token)
):
    return blog_service.update_blog(db, blog_id, blog)

@router.delete("/{blog_id}", summary="删除博客内容", response_model=bool)
def delete_blog(
        blog_id: int = Path(..., ge=1, description="博客ID"),
        db: Session = Depends(get_db),
        _=Depends(verify_token)
):
    return blog_service.delete_blog(db, blog_id) 

@router.put("/{blog_id}/comment", summary="新增博客评论", response_model=bool)
def add_blog_comment(
        request: Request,
        blog_id: int = Path(..., ge=1, description="博客ID"),
        blog_comment: BlogCommentCreate = Body(..., description="博客评论数据"),
        db: Session = Depends(get_db)
):
    return blog_service.add_blog_comment(db, request, blog_id, blog_comment)    

  路由装饰器参数中的response_model表示定义接口返回的数据模型。
  参数注解中的Query()表示参数来自URL查询字符串Path/Query/Body分别对应路径参数、查询参数、请求体参数。
Depends()表示依赖注入,自动解析参数或执行依赖函数。
db: Session = Depends(get_db)表示获取数据库连接,_=Depends(verify_token)表示验证用户身份。
request: Request可以获取到HTTP请求的完整上下文信息。

::: tip 这里的query: BlogQuery = Depends()会从查询参数中自动实例化BlogQuery对象,并进行数据验证和类型转换。
:::

  注册路由:

from fastapi import FastAPI
from .blog import router as blog_router

def register_routers(app: FastAPI):
    app.include_router(blog_router, prefix="")


# main.py
app = FastAPI(title="Blog Service")    
register_routers(app)  

五、中间件

5.1 全局异常处理器

from fastapi import Request, HTTPException, status
from fastapi.responses import JSONResponse
from sqlalchemy.exc import SQLAlchemyError
from config.logging import logger


# 自定义异常类
class AppException(Exception):
    def __init__(self, message: str, details=None):
        self.message = message
        self.details = details


# 全局异常处理中间件
async def global_exception_handler(request: Request, call_next):
    try:
        # 记录请求信息(可选)
        logger.info(f"请求: {request.method} {request.url}")
        if request.query_params:
            logger.info(f"查询参数: {dict(request.query_params)}")

        response = await call_next(request)

        # 记录响应信息(可选)
        if response.status_code >= 400:
            logger.warning(f"响应: {response.status_code}")

        return response

    except AppException as e:
        # 记录业务异常
        logger.error(f"业务异常: {e.message} - 详情: {e.details}")
        return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST,
                            content={"message": e.message, "details": e.details})

    except SQLAlchemyError as e:
        # 记录数据库异常
        logger.critical(f"数据库异常: {str(e)}")
        return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                            content={"message": "数据库操作失败", "details": str(e)})

    except Exception as e:
        # 记录未知异常(带堆栈信息)
        logger.critical(f"未知异常: {str(e)}")
        return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                            content={"code": 500, "message": "服务器内部错误", "details": str(e)})


def get_credentials_exception() -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )

5.2 日志处理器

import atexit
import sys

from fluent import sender
from loguru import logger

from config.setting import settings

FLUENTD_HOST = settings.FLUENTD_HOST
FLUENTD_PORT = 24224
TOPIC_TAG = 'blog-service'

# 日志级别
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>"
)

FILE_FORMAT = (
    "{time:YYYY-MM-DD HH:mm:ss.SSS} | "
    "{level: <8} | "
    "{name}:{function}:{line} - {message}"
)

fluent_sender = sender.FluentSender(
    tag=TOPIC_TAG,
    host=FLUENTD_HOST,
    port=FLUENTD_PORT,
    buffer_max_size=8 * 1024 * 1024,
    timeout=3.0,
    retry_timeout=60
)


def log_to_fluent(message):
    try:
        record = message.record

        # 构建结构化日志数据
        log_data = {
            'topic': TOPIC_TAG,
            'timestamp': record['time'].timestamp(),
            'level': record['level'].name.lower(),
            'message': record['message'],
            'source': f"{record['file'].path}:{record['line']}",
            'module': record['module'],
            'function': record['function'],
            'process_id': record['process'].id,
            'thread_id': record['thread'].id,
            **record['extra']
        }

        if not fluent_sender.emit(TOPIC_TAG, log_data):
            print(f"Fluentd 发送失败: {fluent_sender.last_error}")

    except Exception as e:
        print(f"日志处理异常: {str(e)}")


# 移除默认处理器
logger.remove()

# 添加控制台处理器
logger.add(
    sink=sys.stdout,
    level=LOG_LEVEL,
    format=STDOUT_FORMAT,
    colorize=True,
    backtrace=True,  # 显示完整异常堆栈
    diagnose=True,  # 显示详细异常信息
)

if settings.ENVIRONMENT == 'docker':
    logger.add(
        log_to_fluent,
        level=LOG_LEVEL,  # 处理 INFO 及以上级别
        format="{message}",  # 原始消息(实际使用结构化数据)
        backtrace=True,  # 启用堆栈回溯
        diagnose=True  # 显示诊断信息
    )

    atexit.register(fluent_sender.close)

# 导出配置好的logger
__all__ = ["logger"]