添加日志

This commit is contained in:
ChengCan
2026-03-26 11:25:34 +08:00
parent 3e3f4390a6
commit cff7ddf220
9 changed files with 351 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
import hashlib
import logging
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import text
from sqlalchemy.orm import Session
@@ -17,10 +18,22 @@ except ModuleNotFoundError:
router = APIRouter(prefix="/auth", tags=["auth"])
logger = logging.getLogger("app.auth")
def _mask_username(username: str) -> str:
if len(username) <= 2:
return "*" * len(username)
return f"{username[:2]}***"
@router.post("/login", response_model=LoginResponse)
def login(payload: LoginRequest, db: Session = Depends(get_db)) -> LoginResponse:
def login(
payload: LoginRequest,
request: Request,
db: Session = Depends(get_db),
) -> LoginResponse:
username_masked = _mask_username(payload.username)
with db.begin():
row = (
db.execute(
@@ -45,12 +58,31 @@ def login(payload: LoginRequest, db: Session = Depends(get_db)) -> LoginResponse
.first()
)
if not row:
logger.warning(
"login failed",
extra={
"event": "login",
"request_id": getattr(request.state, "request_id", None),
"username": username_masked,
"reason": "invalid_username_or_password",
},
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="invalid username or password",
)
if int(row["auth_status"]) != 1 or int(row["user_status"]) != 1:
logger.warning(
"login failed",
extra={
"event": "login",
"request_id": getattr(request.state, "request_id", None),
"username": username_masked,
"user_id": int(row["user_id"]),
"reason": "account_disabled",
},
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="account is disabled",
@@ -59,6 +91,15 @@ def login(payload: LoginRequest, db: Session = Depends(get_db)) -> LoginResponse
# Seed data currently stores sha256; migrate to bcrypt/argon2 in production.
password_hash = hashlib.sha256(payload.password.encode("utf-8")).hexdigest()
if not row["password_hash"] or password_hash != row["password_hash"]:
logger.warning(
"login failed",
extra={
"event": "login",
"request_id": getattr(request.state, "request_id", None),
"username": username_masked,
"reason": "invalid_username_or_password",
},
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="invalid username or password",
@@ -88,6 +129,15 @@ def login(payload: LoginRequest, db: Session = Depends(get_db)) -> LoginResponse
)
access_token, expires_in = create_access_token(user_id=int(row["user_id"]))
logger.info(
"login succeeded",
extra={
"event": "login",
"request_id": getattr(request.state, "request_id", None),
"user_id": int(row["user_id"]),
"username": username_masked,
},
)
return LoginResponse(
access_token=access_token,
expires_in=expires_in,

View File

@@ -1,8 +1,9 @@
import json
import logging
from collections.abc import Mapping
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
from sqlalchemy import text
from sqlalchemy.orm import Session
@@ -29,6 +30,7 @@ except ModuleNotFoundError:
router = APIRouter(prefix="/messages", tags=["messages"])
logger = logging.getLogger("app.messages")
def _build_preview(content_type: int, content_text: str | None) -> str:
@@ -141,6 +143,7 @@ def _assert_conversation_access(
@router.post("", response_model=MessageCreateResponse, status_code=status.HTTP_201_CREATED)
def create_message(
payload: MessageCreateRequest,
request: Request,
response: Response,
current_user_id: int = Depends(get_current_user_id),
db: Session = Depends(get_db),
@@ -223,6 +226,19 @@ def create_message(
existing = _get_existing_message(db, conversation_id, payload.client_msg_id)
if existing:
logger.info(
"message idempotent hit",
extra={
"event": "message_create",
"request_id": getattr(request.state, "request_id", None),
"user_id": sender_user_id,
"conversation_id": conversation_id,
"message_id": int(existing["id"]),
"seq": int(existing["seq"]),
"client_msg_id": payload.client_msg_id,
"idempotent": True,
},
)
response.status_code = status.HTTP_200_OK
return MessageCreateResponse(
idempotent=True,
@@ -328,6 +344,19 @@ def create_message(
if not created:
raise HTTPException(status_code=500, detail="failed to load created message")
logger.info(
"message created",
extra={
"event": "message_create",
"request_id": getattr(request.state, "request_id", None),
"user_id": sender_user_id,
"conversation_id": conversation_id,
"message_id": int(created["id"]),
"seq": int(created["seq"]),
"client_msg_id": payload.client_msg_id,
"idempotent": False,
},
)
return MessageCreateResponse(
idempotent=False,
message=_row_to_message_item(created),
@@ -336,6 +365,7 @@ def create_message(
@router.get("", response_model=MessageListResponse)
def list_messages(
request: Request,
conversation_id: int = Query(gt=0),
cursor_seq: int | None = Query(default=None, ge=1),
limit: int = Query(default=20, ge=1, le=100),
@@ -382,6 +412,19 @@ def list_messages(
items = [_row_to_message_item(row) for row in rows]
next_cursor_seq = items[0].seq if has_more and items else None
logger.info(
"messages listed",
extra={
"event": "message_list",
"request_id": getattr(request.state, "request_id", None),
"user_id": current_user_id,
"conversation_id": conversation_id,
"cursor_seq": cursor_seq,
"limit": limit,
"count": len(items),
"has_more": has_more,
},
)
return MessageListResponse(
conversation_id=conversation_id,
has_more=has_more,