mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
309
_deprecated_py_src/services/usage/telemetry_writer.py
Normal file
309
_deprecated_py_src/services/usage/telemetry_writer.py
Normal file
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
Telemetry writer abstraction for stream usage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
from src.clients.redis_client import get_usage_queue_redis_client as get_redis_client
|
||||
from src.config.settings import config
|
||||
from src.core.logger import logger
|
||||
from src.services.usage.events import UsageEventType, build_usage_event
|
||||
from src.services.usage.telemetry import MessageTelemetry
|
||||
|
||||
|
||||
class TelemetryWriter(ABC):
|
||||
def supports_background_submission(self) -> bool:
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
async def record_success(self, **kwargs: Any) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def record_failure(self, **kwargs: Any) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def record_cancelled(self, **kwargs: Any) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DbTelemetryWriter(TelemetryWriter):
|
||||
"""通过 MessageTelemetry 写入数据库的 Writer"""
|
||||
|
||||
# MessageTelemetry 不支持的参数,需要过滤掉
|
||||
# - request_type: MessageTelemetry 内部固定为 "chat",无需外部传入
|
||||
# - metadata: 由本 writer 映射到 request_metadata(用于落库追踪信息)
|
||||
_IGNORED_KWARGS = frozenset({"request_type"})
|
||||
|
||||
def __init__(self, telemetry: MessageTelemetry) -> None:
|
||||
self._telemetry = telemetry
|
||||
|
||||
def _filter_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||
"""过滤掉 MessageTelemetry 不支持的参数"""
|
||||
out = {k: v for k, v in kwargs.items() if k not in self._IGNORED_KWARGS}
|
||||
# 兼容 stream 侧传入的 metadata 字段:映射到 MessageTelemetry 的 request_metadata
|
||||
if "metadata" in out and "request_metadata" not in out:
|
||||
out["request_metadata"] = out.get("metadata")
|
||||
out.pop("metadata", None)
|
||||
return out
|
||||
|
||||
async def record_success(self, **kwargs: Any) -> None:
|
||||
await self._telemetry.record_success(**self._filter_kwargs(kwargs))
|
||||
|
||||
async def record_failure(self, **kwargs: Any) -> None:
|
||||
await self._telemetry.record_failure(**self._filter_kwargs(kwargs))
|
||||
|
||||
async def record_cancelled(self, **kwargs: Any) -> None:
|
||||
await self._telemetry.record_cancelled(**self._filter_kwargs(kwargs))
|
||||
|
||||
|
||||
class QueueTelemetryWriter(TelemetryWriter):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
request_id: str,
|
||||
user_id: str,
|
||||
api_key_id: str,
|
||||
log_level: str = "basic",
|
||||
sensitive_headers: list[str] | None = None,
|
||||
max_request_body_size: int = 0,
|
||||
max_response_body_size: int = 0,
|
||||
) -> None:
|
||||
self.request_id = request_id
|
||||
self.user_id = user_id
|
||||
self.api_key_id = api_key_id
|
||||
self.log_level = (log_level or "basic").strip().lower()
|
||||
self._sensitive_headers = sensitive_headers or [
|
||||
"authorization",
|
||||
"x-api-key",
|
||||
"api-key",
|
||||
"cookie",
|
||||
"set-cookie",
|
||||
]
|
||||
self._max_request_body_size = int(max_request_body_size or 0)
|
||||
self._max_response_body_size = int(max_response_body_size or 0)
|
||||
|
||||
@property
|
||||
def include_headers(self) -> bool:
|
||||
return self.log_level in {"headers", "full"}
|
||||
|
||||
@property
|
||||
def include_bodies(self) -> bool:
|
||||
return self.log_level == "full"
|
||||
|
||||
def supports_background_submission(self) -> bool:
|
||||
return True
|
||||
|
||||
async def record_success(self, **kwargs: Any) -> None:
|
||||
await self._publish_event(UsageEventType.COMPLETED, **kwargs)
|
||||
|
||||
async def record_failure(self, **kwargs: Any) -> None:
|
||||
await self._publish_event(UsageEventType.FAILED, **kwargs)
|
||||
|
||||
async def record_cancelled(self, **kwargs: Any) -> None:
|
||||
await self._publish_event(UsageEventType.CANCELLED, **kwargs)
|
||||
|
||||
async def _publish_event(self, event_type: UsageEventType, **kwargs: Any) -> None:
|
||||
redis_client = await get_redis_client(require_redis=False)
|
||||
if not redis_client:
|
||||
raise RuntimeError("Redis unavailable for usage queue")
|
||||
|
||||
data = self._build_event_data(**kwargs)
|
||||
event = build_usage_event(
|
||||
event_type=event_type,
|
||||
request_id=self.request_id,
|
||||
data=data,
|
||||
)
|
||||
maxlen = config.usage_queue_stream_maxlen
|
||||
try:
|
||||
if maxlen > 0:
|
||||
await redis_client.xadd(
|
||||
config.usage_queue_stream_key,
|
||||
event.to_stream_fields(),
|
||||
maxlen=maxlen,
|
||||
approximate=True,
|
||||
)
|
||||
else:
|
||||
await redis_client.xadd(config.usage_queue_stream_key, event.to_stream_fields())
|
||||
except Exception as exc:
|
||||
logger.error("[usage-queue] XADD failed: {}", exc)
|
||||
raise
|
||||
|
||||
def _mask_headers(self, headers: Any) -> Any:
|
||||
"""Mask sensitive headers before putting them into Redis."""
|
||||
if not isinstance(headers, dict) or not headers:
|
||||
return headers
|
||||
sensitive = {h.lower() for h in self._sensitive_headers if isinstance(h, str) and h}
|
||||
if not sensitive:
|
||||
return headers
|
||||
out: dict[str, Any] = {}
|
||||
for k, v in headers.items():
|
||||
key = str(k)
|
||||
if key.lower() in sensitive:
|
||||
s = str(v)
|
||||
if len(s) > 8:
|
||||
out[key] = s[:4] + "****" + s[-4:]
|
||||
else:
|
||||
out[key] = "****"
|
||||
else:
|
||||
out[key] = v
|
||||
return out
|
||||
|
||||
def _truncate_body(self, value: Any, *, max_size: int, is_request: bool) -> Any:
|
||||
"""Best-effort truncate body based on SystemConfigService max_*_body_size."""
|
||||
if value is None:
|
||||
return None
|
||||
limit = int(max_size or 0)
|
||||
if limit <= 0:
|
||||
return value
|
||||
|
||||
body_str = json.dumps(value) if isinstance(value, (dict, list)) else str(value)
|
||||
if len(body_str) <= limit:
|
||||
return value
|
||||
|
||||
# Match SystemConfigService.truncate_body contract.
|
||||
if isinstance(value, (dict, list)):
|
||||
return {
|
||||
"_truncated": True,
|
||||
"_original_size": len(body_str),
|
||||
"_content": body_str[:limit],
|
||||
}
|
||||
kind = "request" if is_request else "response"
|
||||
return (
|
||||
body_str[:limit]
|
||||
+ f"\n... (truncated {kind} body, original size: {len(body_str)} bytes)"
|
||||
)
|
||||
|
||||
def _build_event_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
# 必需字段
|
||||
data: dict[str, Any] = {
|
||||
"request_id": self.request_id,
|
||||
"user_id": self.user_id,
|
||||
"api_key_id": self.api_key_id,
|
||||
}
|
||||
|
||||
# 可选字段 - 只添加非 None/非默认值,减少 payload 大小
|
||||
# 注意:消费者端需要处理缺失字段的默认值
|
||||
if kwargs.get("provider"):
|
||||
data["provider"] = kwargs["provider"]
|
||||
if kwargs.get("model"):
|
||||
data["model"] = kwargs["model"]
|
||||
if kwargs.get("target_model"):
|
||||
data["target_model"] = kwargs["target_model"]
|
||||
|
||||
# Token 计数 - 0 是常见值,但仍需传递
|
||||
input_tokens = kwargs.get("input_tokens", 0)
|
||||
output_tokens = kwargs.get("output_tokens", 0)
|
||||
if input_tokens:
|
||||
data["input_tokens"] = input_tokens
|
||||
if output_tokens:
|
||||
data["output_tokens"] = output_tokens
|
||||
|
||||
# 缓存 token(cache_creation_tokens -> cache_creation_input_tokens 映射)
|
||||
cache_creation = kwargs.get("cache_creation_tokens", 0)
|
||||
cache_read = kwargs.get("cache_read_tokens", 0)
|
||||
if cache_creation:
|
||||
data["cache_creation_input_tokens"] = cache_creation
|
||||
if cache_read:
|
||||
data["cache_read_input_tokens"] = cache_read
|
||||
|
||||
# 缓存 5m/1h 细分
|
||||
cache_creation_5m = kwargs.get("cache_creation_tokens_5m", 0)
|
||||
cache_creation_1h = kwargs.get("cache_creation_tokens_1h", 0)
|
||||
if cache_creation_5m:
|
||||
data["cache_creation_input_tokens_5m"] = cache_creation_5m
|
||||
if cache_creation_1h:
|
||||
data["cache_creation_input_tokens_1h"] = cache_creation_1h
|
||||
|
||||
# 时间指标
|
||||
if kwargs.get("response_time_ms") is not None:
|
||||
data["response_time_ms"] = kwargs["response_time_ms"]
|
||||
if kwargs.get("first_byte_time_ms") is not None:
|
||||
data["first_byte_time_ms"] = kwargs["first_byte_time_ms"]
|
||||
|
||||
# 状态信息
|
||||
status_code = kwargs.get("status_code", 200)
|
||||
if status_code != 200:
|
||||
data["status_code"] = status_code
|
||||
if kwargs.get("error_message"):
|
||||
data["error_message"] = kwargs["error_message"]
|
||||
|
||||
# 格式信息
|
||||
request_type = kwargs.get("request_type", "chat")
|
||||
if request_type != "chat":
|
||||
data["request_type"] = request_type
|
||||
if kwargs.get("api_format"):
|
||||
data["api_format"] = kwargs["api_format"]
|
||||
if kwargs.get("api_family"):
|
||||
data["api_family"] = kwargs["api_family"]
|
||||
if kwargs.get("endpoint_kind"):
|
||||
data["endpoint_kind"] = kwargs["endpoint_kind"]
|
||||
if kwargs.get("endpoint_api_format"):
|
||||
data["endpoint_api_format"] = kwargs["endpoint_api_format"]
|
||||
if kwargs.get("has_format_conversion"):
|
||||
data["has_format_conversion"] = True
|
||||
|
||||
# 流式标记 - 默认 True,只记录 False
|
||||
if not kwargs.get("is_stream", True):
|
||||
data["is_stream"] = False
|
||||
|
||||
# Provider 追踪
|
||||
if kwargs.get("provider_id"):
|
||||
data["provider_id"] = kwargs["provider_id"]
|
||||
if kwargs.get("provider_endpoint_id"):
|
||||
data["provider_endpoint_id"] = kwargs["provider_endpoint_id"]
|
||||
if kwargs.get("provider_api_key_id"):
|
||||
data["provider_api_key_id"] = kwargs["provider_api_key_id"]
|
||||
|
||||
# 元数据
|
||||
if kwargs.get("metadata"):
|
||||
data["metadata"] = kwargs["metadata"]
|
||||
|
||||
# Optional: Headers (masked)
|
||||
if self.include_headers:
|
||||
for _hdr_key in (
|
||||
"request_headers",
|
||||
"provider_request_headers",
|
||||
"response_headers",
|
||||
"client_response_headers",
|
||||
):
|
||||
if kwargs.get(_hdr_key) is not None:
|
||||
data[_hdr_key] = self._mask_headers(kwargs[_hdr_key])
|
||||
|
||||
# Optional: Bodies (truncated)
|
||||
if self.include_bodies:
|
||||
request_body = self._truncate_body(
|
||||
kwargs.get("request_body"),
|
||||
max_size=self._max_request_body_size,
|
||||
is_request=True,
|
||||
)
|
||||
provider_request_body = self._truncate_body(
|
||||
kwargs.get("provider_request_body"),
|
||||
max_size=self._max_request_body_size,
|
||||
is_request=True,
|
||||
)
|
||||
response_body = self._truncate_body(
|
||||
kwargs.get("response_body"),
|
||||
max_size=self._max_response_body_size,
|
||||
is_request=False,
|
||||
)
|
||||
client_response_body = self._truncate_body(
|
||||
kwargs.get("client_response_body"),
|
||||
max_size=self._max_response_body_size,
|
||||
is_request=False,
|
||||
)
|
||||
if request_body is not None:
|
||||
data["request_body"] = request_body
|
||||
if provider_request_body is not None:
|
||||
data["provider_request_body"] = provider_request_body
|
||||
if response_body is not None:
|
||||
data["response_body"] = response_body
|
||||
if client_response_body is not None:
|
||||
data["client_response_body"] = client_response_body
|
||||
|
||||
return data
|
||||
Reference in New Issue
Block a user