feat(监控): 完善系统监控、登录状态与访问审计能力
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled

This commit is contained in:
Cheng Zhou
2026-07-10 17:11:24 +08:00
parent 400c9be3a7
commit f11a5c84d9
82 changed files with 10283 additions and 1781 deletions
+16 -2
View File
@@ -6,13 +6,15 @@ import uuid
from datetime import datetime
from typing import Optional
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Index, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, Index, String
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.db.base_class import Base
JSONB_TYPE = JSON().with_variant(JSONB, "postgresql")
class PermissionAccessLog(Base):
__tablename__ = "permission_access_logs"
@@ -22,6 +24,9 @@ class PermissionAccessLog(Base):
Index("ix_perm_log_endpoint_created", "endpoint_key", "created_at"),
Index("ix_perm_log_created_at", "created_at"),
Index("ix_perm_log_allowed", "allowed", "created_at"),
Index("ix_perm_log_ip_created", "ip_address", "created_at"),
Index("ix_perm_log_client_source_created", "client_type", "created_at"),
Index("ix_perm_log_request_id", "request_id"),
)
id: Mapped[uuid.UUID] = mapped_column(
@@ -38,6 +43,15 @@ class PermissionAccessLog(Base):
allowed: Mapped[bool] = mapped_column(Boolean, nullable=False)
elapsed_ms: Mapped[float] = mapped_column(Float, nullable=False)
ip_address: Mapped[Optional[str]] = mapped_column(String(45), nullable=True)
user_agent: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
client_type: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)
client_version: Mapped[Optional[str]] = mapped_column(String(32), nullable=True)
client_platform: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)
build_channel: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)
build_commit: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
request_headers: Mapped[Optional[dict]] = mapped_column(JSONB_TYPE, nullable=True)
request_snapshot: Mapped[Optional[dict]] = mapped_column(JSONB_TYPE, nullable=True)
request_id: Mapped[Optional[str]] = mapped_column(String(36), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
+12 -2
View File
@@ -6,13 +6,15 @@ import uuid
from datetime import datetime
from typing import Optional
from sqlalchemy import DateTime, Float, Index, Integer, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy import JSON, DateTime, Float, Index, Integer, String
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.db.base_class import Base
JSONB_TYPE = JSON().with_variant(JSONB, "postgresql")
class SecurityAccessLog(Base):
__tablename__ = "security_access_logs"
@@ -21,6 +23,9 @@ class SecurityAccessLog(Base):
Index("ix_security_log_ip_created", "client_ip", "created_at"),
Index("ix_security_log_status_created", "status_code", "created_at"),
Index("ix_security_log_auth_created", "auth_status", "created_at"),
Index("ix_security_log_request_id", "request_id"),
Index("ix_security_log_category_created", "category", "created_at"),
Index("ix_security_log_severity_created", "severity", "created_at"),
)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
@@ -35,8 +40,13 @@ class SecurityAccessLog(Base):
client_platform: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)
build_channel: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)
build_commit: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
request_headers: Mapped[Optional[dict]] = mapped_column(JSONB_TYPE, nullable=True)
request_snapshot: Mapped[Optional[dict]] = mapped_column(JSONB_TYPE, nullable=True)
auth_status: Mapped[str] = mapped_column(String(30), nullable=False)
user_identifier: Mapped[Optional[str]] = mapped_column(String(80), nullable=True)
request_id: Mapped[Optional[str]] = mapped_column(String(36), nullable=True)
category: Mapped[Optional[str]] = mapped_column(String(30), nullable=True)
severity: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
@@ -0,0 +1,48 @@
"""Hourly source-location rollup for monitoring analytics."""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Float, Index, Integer, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.db.base_class import Base
class SourceLocationSnapshot(Base):
__tablename__ = "source_location_snapshots"
__table_args__ = (
UniqueConstraint("bucket_time", "ip_hash", "user_hash", name="uq_source_location_bucket_identity"),
Index("ix_source_location_bucket", "bucket_time"),
Index("ix_source_location_country_bucket", "country_code", "bucket_time"),
Index("ix_source_location_risk_bucket", "high_risk_count", "bucket_time"),
)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
bucket_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
ip_hash: Mapped[str] = mapped_column(String(64), nullable=False)
user_hash: Mapped[str] = mapped_column(String(64), nullable=False, default="")
country: Mapped[str] = mapped_column(String(100), nullable=False, default="")
country_code: Mapped[str] = mapped_column(String(16), nullable=False, default="")
province: Mapped[str] = mapped_column(String(100), nullable=False, default="")
region_code: Mapped[str] = mapped_column(String(24), nullable=False, default="")
city: Mapped[str] = mapped_column(String(100), nullable=False, default="")
isp: Mapped[str] = mapped_column(String(160), nullable=False, default="")
location: Mapped[str] = mapped_column(String(320), nullable=False, default="")
longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
accuracy_level: Mapped[str] = mapped_column(String(16), nullable=False, default="unknown")
allowed_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
denied_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
security_event_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
high_risk_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
auth_failure_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
first_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base_class import Base
class UserLoginSession(Base):
"""Server-side login activity record without storing credentials or raw IPs."""
__tablename__ = "user_login_sessions"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True)
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
client_type: Mapped[str] = mapped_column(String(16), nullable=False, default="web")
client_platform: Mapped[str | None] = mapped_column(String(32), nullable=True)
client_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
client_source: Mapped[str | None] = mapped_column(String(32), nullable=True)
login_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
end_reason: Mapped[str | None] = mapped_column(String(32), nullable=True)