Files
droid2api/log-extractor.js
Claude Code 82a5a2cdfb feat: 集成阿里云日志服务(SLS)并增强日志记录详情
- 添加 SLS 日志上报模块(sls-logger.js)
  - 支持批量上报(每10条或5秒间隔)
  - 环境变量缺失时静默降级
  - 自动重试失败的日志

- 新增日志信息提取器(log-extractor.js)
  - 提取 Token 使用统计(input_tokens, output_tokens)
  - 提取用户标识信息(user_id, session_id, ip)
  - 提取请求参数(temperature, max_tokens, stream)
  - 提取消息摘要(message_count, role_distribution, tool_names)

- 增强所有 API 端点的日志记录
  - /v1/chat/completions
  - /v1/responses
  - /v1/messages
  - /v1/messages/count_tokens

- 修复日志字段序列化问题
  - 扁平化嵌套对象字段,避免 [object Object]
  - 数组字段转换为逗号分隔字符串

- 添加阿里云环境变量配置到 docker-compose.yml
  - ALIYUN_ACCESS_KEY_ID
  - ALIYUN_ACCESS_KEY_SECRET
  - ALIYUN_SLS_ENDPOINT
  - ALIYUN_SLS_PROJECT
  - ALIYUN_SLS_LOGSTORE

- 修改认证配置为自动刷新 Token 机制
  - 使用 DROID_REFRESH_KEY 替代固定的 FACTORY_API_KEY
  - 实现每6小时自动刷新(Token有效期8小时)
  - Token 持久化到 auth.json
2025-12-27 04:42:43 +00:00

215 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 日志信息提取辅助函数
*
* 用于从请求和响应中提取详细的日志信息
*/
/**
* 提取用户标识信息
* @param {Object} req - Express 请求对象
* @returns {Object} 用户标识信息
*/
export function extractUserInfo(req) {
const userInfo = {};
// 从请求头提取用户标识
if (req.headers['x-user-id']) {
userInfo.user_id = req.headers['x-user-id'];
}
// 从 metadata 中提取用户信息Anthropic API
if (req.body?.metadata?.user_id) {
userInfo.user_id = req.body.metadata.user_id;
}
// 提取客户端信息
if (req.headers['user-agent']) {
userInfo.user_agent = req.headers['user-agent'];
}
// 提取会话ID
if (req.headers['x-session-id']) {
userInfo.session_id = req.headers['x-session-id'];
}
// 提取 IP 地址
userInfo.ip = req.ip || req.connection?.remoteAddress;
return userInfo;
}
/**
* 提取请求参数(扁平化)
* @param {Object} reqBody - 请求体
* @returns {Object} 扁平化的请求参数
*/
export function extractRequestParams(reqBody) {
const params = {};
if (reqBody?.temperature !== undefined) {
params.param_temperature = reqBody.temperature;
}
if (reqBody?.max_tokens !== undefined) {
params.param_max_tokens = reqBody.max_tokens;
}
if (reqBody?.top_p !== undefined) {
params.param_top_p = reqBody.top_p;
}
if (reqBody?.stream !== undefined) {
params.param_stream = reqBody.stream;
}
return params;
}
/**
* 提取消息摘要
* @param {Object} reqBody - 请求体
* @returns {Object} 消息摘要信息
*/
export function extractMessageSummary(reqBody) {
const summary = {};
// Anthropic API 格式 (/v1/messages)
if (reqBody?.messages && Array.isArray(reqBody.messages)) {
summary.message_count = reqBody.messages.length;
// 提取第一条消息的前100字符作为摘要
if (reqBody.messages.length > 0) {
const firstMsg = reqBody.messages[0];
if (firstMsg.content) {
if (typeof firstMsg.content === 'string') {
summary.first_message = firstMsg.content.substring(0, 100);
} else if (Array.isArray(firstMsg.content)) {
// 处理多模态内容
const textContent = firstMsg.content.find(c => c.type === 'text');
if (textContent?.text) {
summary.first_message = textContent.text.substring(0, 100);
}
}
}
summary.first_message_role = firstMsg.role;
}
// 统计角色分布(扁平化)
const roles = reqBody.messages.map(m => m.role);
summary.role_user_count = roles.filter(r => r === 'user').length;
summary.role_assistant_count = roles.filter(r => r === 'assistant').length;
summary.role_system_count = roles.filter(r => r === 'system').length;
}
// System prompt
if (reqBody?.system) {
summary.has_system_prompt = true;
summary.system_prompt_length = typeof reqBody.system === 'string'
? reqBody.system.length
: JSON.stringify(reqBody.system).length;
}
// Tools
if (reqBody?.tools && Array.isArray(reqBody.tools)) {
summary.tool_count = reqBody.tools.length;
// 将工具名数组转换为逗号分隔的字符串最多记录10个
summary.tool_names = reqBody.tools.map(t => t.name).slice(0, 10).join(',');
}
return summary;
}
/**
* 提取 Token 使用统计
* @param {Object} responseData - API 响应数据
* @returns {Object} Token 统计信息
*/
export function extractTokenUsage(responseData) {
const usage = {};
if (responseData?.usage) {
if (responseData.usage.input_tokens !== undefined) {
usage.input_tokens = responseData.usage.input_tokens;
}
if (responseData.usage.output_tokens !== undefined) {
usage.output_tokens = responseData.usage.output_tokens;
}
if (responseData.usage.total_tokens !== undefined) {
usage.total_tokens = responseData.usage.total_tokens;
}
// 缓存相关 tokens
if (responseData.usage.cache_creation_input_tokens !== undefined) {
usage.cache_creation_input_tokens = responseData.usage.cache_creation_input_tokens;
}
if (responseData.usage.cache_read_input_tokens !== undefined) {
usage.cache_read_input_tokens = responseData.usage.cache_read_input_tokens;
}
}
return usage;
}
/**
* 构建完整的日志对象
* @param {Object} options - 日志选项
* @param {string} options.method - HTTP 方法
* @param {string} options.endpoint - 请求端点
* @param {string} options.model - 模型 ID
* @param {number} options.status - 响应状态码
* @param {number} options.duration_ms - 请求耗时
* @param {Object} options.req - Express 请求对象
* @param {Object} [options.responseData] - 响应数据(可选,非流式响应时提供)
* @param {string} [options.error] - 错误信息(可选)
* @returns {Object} 完整的日志对象
*/
export function buildDetailedLog(options) {
const { method, endpoint, model, status, duration_ms, req, responseData, error } = options;
const log = {
method,
endpoint,
model,
status,
duration_ms
};
// 添加错误信息
if (error) {
log.error = error;
}
// 提取用户信息
if (req) {
const userInfo = extractUserInfo(req);
Object.assign(log, userInfo);
// 提取请求参数(已扁平化)
const params = extractRequestParams(req.body);
Object.assign(log, params);
// 提取消息摘要
const summary = extractMessageSummary(req.body);
if (Object.keys(summary).length > 0) {
Object.assign(log, summary);
}
}
// 提取 Token 使用统计
if (responseData) {
const tokenUsage = extractTokenUsage(responseData);
if (Object.keys(tokenUsage).length > 0) {
Object.assign(log, tokenUsage);
}
// 添加响应信息
if (responseData.id) {
log.response_id = responseData.id;
}
if (responseData.stop_reason) {
log.stop_reason = responseData.stop_reason;
}
}
return log;
}