feat: 集成阿里云日志服务(SLS)

- 添加 aliyun-log SDK 依赖
- 新增 sls-logger.js 模块,支持批量日志上报、静默降级
- 在四个 API 处理函数中集成请求日志记录
- 更新 .env.example 添加 SLS 配置示例
This commit is contained in:
empty
2025-12-27 03:08:01 +08:00
parent dec2f26b5c
commit eb1096ce54
5 changed files with 379 additions and 32 deletions

View File

@@ -5,3 +5,10 @@ FACTORY_API_KEY=your_factory_api_key_here
# 方式2使用refresh token自动刷新次优先级
DROID_REFRESH_KEY=your_refresh_token_here
# 阿里云日志服务配置
ALIYUN_ACCESS_KEY_ID=your_access_key_id
ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret
ALIYUN_SLS_ENDPOINT=cn-hangzhou.log.aliyuncs.com
ALIYUN_SLS_PROJECT=your_project_name
ALIYUN_SLS_LOGSTORE=your_logstore_name

173
package-lock.json generated
View File

@@ -1,19 +1,99 @@
{
"name": "droid2api",
"version": "1.3.5",
"version": "1.3.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "droid2api",
"version": "1.3.5",
"version": "1.3.7",
"license": "MIT",
"dependencies": {
"aliyun-log": "github:aliyun/aliyun-log-nodejs-sdk",
"express": "^4.18.2",
"https-proxy-agent": "^7.0.2",
"node-fetch": "^3.3.2"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
@@ -36,6 +116,18 @@
"node": ">= 14"
}
},
"node_modules/aliyun-log": {
"name": "@alicloud/log",
"version": "1.2.6",
"resolved": "git+ssh://git@github.com/aliyun/aliyun-log-nodejs-sdk.git#f5c2ab9cf5e0c7d3edd2fa1a15fc7f0a9946cd05",
"license": "MIT",
"dependencies": {
"debug": "^2.6.8",
"httpx": "^2.1.2",
"kitx": "^1.2.1",
"protobufjs": "^6.8.8"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -502,6 +594,39 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/httpx": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/httpx/-/httpx-2.3.3.tgz",
"integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==",
"license": "MIT",
"dependencies": {
"@types/node": "^20",
"debug": "^4.1.1"
}
},
"node_modules/httpx/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/httpx/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -529,6 +654,18 @@
"node": ">= 0.10"
}
},
"node_modules/kitx": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/kitx/-/kitx-1.3.0.tgz",
"integrity": "sha512-fhBqFlXd0GkKTB+8ayLfpzPUw+LHxZlPAukPNBD1Om7JMeInT+/PxCAf1yLagvD+VKoyWhXtJR68xQkX/a0wOQ==",
"license": "MIT"
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
"license": "Apache-2.0"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -690,6 +827,32 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/protobufjs": {
"version": "6.11.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
"integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^4.0.0"
},
"bin": {
"pbjs": "bin/pbjs",
"pbts": "bin/pbts"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -931,6 +1094,12 @@
"node": ">= 0.6"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",

View File

@@ -8,12 +8,17 @@
"start": "node server.js",
"dev": "node server.js"
},
"keywords": ["openai", "api", "proxy"],
"keywords": [
"openai",
"api",
"proxy"
],
"author": "",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"https-proxy-agent": "^7.0.2",
"node-fetch": "^3.3.2"
"node-fetch": "^3.3.2",
"aliyun-log": "github:aliyun/aliyun-log-nodejs-sdk"
}
}
}

View File

@@ -9,6 +9,7 @@ import { AnthropicResponseTransformer } from './transformers/response-anthropic.
import { OpenAIResponseTransformer } from './transformers/response-openai.js';
import { getApiKey } from './auth.js';
import { getNextProxyAgent } from './proxy-manager.js';
import { logRequest as slsLogRequest } from './sls-logger.js';
const router = express.Router();
@@ -52,7 +53,7 @@ function convertResponseToChatCompletion(resp) {
router.get('/v1/models', (req, res) => {
logInfo('GET /v1/models');
try {
const config = getConfig();
const models = config.models.map(model => ({
@@ -81,6 +82,7 @@ router.get('/v1/models', (req, res) => {
// 标准 OpenAI 聊天补全处理函数(带格式转换)
async function handleChatCompletions(req, res) {
logInfo('POST /v1/chat/completions');
const startTime = Date.now();
try {
const openaiRequest = req.body;
@@ -108,7 +110,7 @@ async function handleChatCompletions(req, res) {
authHeader = await getApiKey(req.headers.authorization);
} catch (error) {
logError('Failed to get API key', error);
return res.status(500).json({
return res.status(500).json({
error: 'API key not available',
message: 'Failed to get or refresh API key. Please check server logs.'
});
@@ -166,9 +168,9 @@ async function handleChatCompletions(req, res) {
if (!response.ok) {
const errorText = await response.text();
logError(`Endpoint error: ${response.status}`, new Error(errorText));
return res.status(response.status).json({
return res.status(response.status).json({
error: `Endpoint returned ${response.status}`,
details: errorText
details: errorText
});
}
@@ -187,6 +189,7 @@ async function handleChatCompletions(req, res) {
}
res.end();
logInfo('Stream forwarded (common type)');
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime });
} catch (streamError) {
logError('Stream error', streamError);
res.end();
@@ -206,6 +209,7 @@ async function handleChatCompletions(req, res) {
}
res.end();
logInfo('Stream completed');
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime });
} catch (streamError) {
logError('Stream error', streamError);
res.end();
@@ -228,13 +232,15 @@ async function handleChatCompletions(req, res) {
logResponse(200, null, data);
res.json(data);
}
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime });
}
} catch (error) {
logError('Error in /v1/chat/completions', error);
res.status(500).json({
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
res.status(500).json({
error: 'Internal server error',
message: error.message
message: error.message
});
}
}
@@ -242,6 +248,7 @@ async function handleChatCompletions(req, res) {
// 直接转发 OpenAI 请求(不做格式转换)
async function handleDirectResponses(req, res) {
logInfo('POST /v1/responses');
const startTime = Date.now();
try {
const openaiRequest = req.body;
@@ -258,7 +265,7 @@ async function handleDirectResponses(req, res) {
// 只允许 openai 类型端点
if (model.type !== 'openai') {
return res.status(400).json({
return res.status(400).json({
error: 'Invalid endpoint type',
message: `/v1/responses 接口只支持 openai 类型端点,当前模型 ${modelId}${model.type} 类型`
});
@@ -280,17 +287,17 @@ async function handleDirectResponses(req, res) {
authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey);
} catch (error) {
logError('Failed to get API key', error);
return res.status(500).json({
return res.status(500).json({
error: 'API key not available',
message: 'Failed to get or refresh API key. Please check server logs.'
});
}
const clientHeaders = req.headers;
// Get provider from model config
const provider = getModelProvider(modelId);
// 获取 headers
const headers = getOpenAIHeaders(authHeader, clientHeaders, provider);
@@ -340,8 +347,8 @@ async function handleDirectResponses(req, res) {
if (claudeCodeTools.includes(tool.name)) return false;
// 过滤所有 mcp__ 开头的工具和 MCP 相关工具
return !tool.name.startsWith('mcp__') &&
!tool.name.includes('Mcp') &&
!tool.name.includes('MCP');
!tool.name.includes('Mcp') &&
!tool.name.includes('MCP');
});
}
@@ -366,9 +373,9 @@ async function handleDirectResponses(req, res) {
if (!response.ok) {
const errorText = await response.text();
logError(`Endpoint error: ${response.status}`, new Error(errorText));
return res.status(response.status).json({
return res.status(response.status).json({
error: `Endpoint returned ${response.status}`,
details: errorText
details: errorText
});
}
@@ -387,6 +394,7 @@ async function handleDirectResponses(req, res) {
}
res.end();
logInfo('Stream forwarded successfully');
slsLogRequest({ method: 'POST', endpoint: '/v1/responses', model: modelId, status: 200, duration_ms: Date.now() - startTime });
} catch (streamError) {
logError('Stream error', streamError);
res.end();
@@ -396,13 +404,15 @@ async function handleDirectResponses(req, res) {
const data = await response.json();
logResponse(200, null, data);
res.json(data);
slsLogRequest({ method: 'POST', endpoint: '/v1/responses', model: modelId, status: 200, duration_ms: Date.now() - startTime });
}
} catch (error) {
logError('Error in /v1/responses', error);
res.status(500).json({
slsLogRequest({ method: 'POST', endpoint: '/v1/responses', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
res.status(500).json({
error: 'Internal server error',
message: error.message
message: error.message
});
}
}
@@ -410,6 +420,7 @@ async function handleDirectResponses(req, res) {
// 直接转发 Anthropic 请求(不做格式转换)
async function handleDirectMessages(req, res) {
logInfo('POST /v1/messages');
const startTime = Date.now();
try {
const anthropicRequest = req.body;
@@ -426,7 +437,7 @@ async function handleDirectMessages(req, res) {
// 只允许 anthropic 类型端点
if (model.type !== 'anthropic') {
return res.status(400).json({
return res.status(400).json({
error: 'Invalid endpoint type',
message: `/v1/messages 接口只支持 anthropic 类型端点,当前模型 ${modelId}${model.type} 类型`
});
@@ -448,17 +459,17 @@ async function handleDirectMessages(req, res) {
authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey);
} catch (error) {
logError('Failed to get API key', error);
return res.status(500).json({
return res.status(500).json({
error: 'API key not available',
message: 'Failed to get or refresh API key. Please check server logs.'
});
}
const clientHeaders = req.headers;
// Get provider from model config
const provider = getModelProvider(modelId);
// 获取 headers
const isStreaming = anthropicRequest.stream === true;
const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId, provider);
@@ -519,7 +530,7 @@ async function handleDirectMessages(req, res) {
'high': 24576,
'xhigh': 40960
};
modifiedRequest.thinking = {
type: 'enabled',
budget_tokens: budgetTokens[reasoningLevel]
@@ -575,8 +586,8 @@ async function handleDirectMessages(req, res) {
if (claudeCodeTools.includes(tool.name)) return false;
// 过滤所有 mcp__ 开头的工具和 MCP 相关工具
return !tool.name.startsWith('mcp__') &&
!tool.name.includes('Mcp') &&
!tool.name.includes('MCP');
!tool.name.includes('Mcp') &&
!tool.name.includes('MCP');
});
}
@@ -601,9 +612,9 @@ async function handleDirectMessages(req, res) {
if (!response.ok) {
const errorText = await response.text();
logError(`Endpoint error: ${response.status}`, new Error(errorText));
return res.status(response.status).json({
return res.status(response.status).json({
error: `Endpoint returned ${response.status}`,
details: errorText
details: errorText
});
}
@@ -620,6 +631,7 @@ async function handleDirectMessages(req, res) {
}
res.end();
logInfo('Stream forwarded successfully');
slsLogRequest({ method: 'POST', endpoint: '/v1/messages', model: modelId, status: 200, duration_ms: Date.now() - startTime });
} catch (streamError) {
logError('Stream error', streamError);
res.end();
@@ -629,10 +641,12 @@ async function handleDirectMessages(req, res) {
const data = await response.json();
logResponse(200, null, data);
res.json(data);
slsLogRequest({ method: 'POST', endpoint: '/v1/messages', model: modelId, status: 200, duration_ms: Date.now() - startTime });
}
} catch (error) {
logError('Error in /v1/messages', error);
slsLogRequest({ method: 'POST', endpoint: '/v1/messages', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
res.status(500).json({
error: 'Internal server error',
message: error.message
@@ -643,6 +657,7 @@ async function handleDirectMessages(req, res) {
// 处理 Anthropic count_tokens 请求
async function handleCountTokens(req, res) {
logInfo('POST /v1/messages/count_tokens');
const startTime = Date.now();
try {
const anthropicRequest = req.body;
@@ -686,10 +701,10 @@ async function handleCountTokens(req, res) {
}
const clientHeaders = req.headers;
// Get provider from model config
const provider = getModelProvider(modelId);
const headers = getAnthropicHeaders(authHeader, clientHeaders, false, modelId, provider);
// 构建 count_tokens 端点 URL
@@ -728,9 +743,11 @@ async function handleCountTokens(req, res) {
const data = await response.json();
logResponse(200, null, data);
res.json(data);
slsLogRequest({ method: 'POST', endpoint: '/v1/messages/count_tokens', model: modelId, status: 200, duration_ms: Date.now() - startTime });
} catch (error) {
logError('Error in /v1/messages/count_tokens', error);
slsLogRequest({ method: 'POST', endpoint: '/v1/messages/count_tokens', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
res.status(500).json({
error: 'Internal server error',
message: error.message

149
sls-logger.js Normal file
View File

@@ -0,0 +1,149 @@
/**
* 阿里云日志服务SLS日志模块
*
* 功能:
* - 将 API 请求/响应日志上报到阿里云 SLS
* - 批量上报,减少 API 调用
* - 环境变量缺失时静默降级
*/
import aliyunLog from 'aliyun-log';
const { Client, PutLogsRequest, LogItem, LogContent } = aliyunLog;
// SLS 配置
const SLS_CONFIG = {
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
endpoint: process.env.ALIYUN_SLS_ENDPOINT,
project: process.env.ALIYUN_SLS_PROJECT,
logstore: process.env.ALIYUN_SLS_LOGSTORE
};
// 检查配置是否完整
const isConfigured = Object.values(SLS_CONFIG).every(v => v);
let client = null;
let logQueue = [];
const BATCH_SIZE = 10;
const FLUSH_INTERVAL_MS = 5000;
// 初始化 SLS Client
function initClient() {
if (!isConfigured) {
console.warn('[SLS] 阿里云日志服务未配置,日志将仅输出到控制台');
return null;
}
try {
client = new Client({
accessKeyId: SLS_CONFIG.accessKeyId,
accessKeySecret: SLS_CONFIG.accessKeySecret,
endpoint: SLS_CONFIG.endpoint
});
console.log('[SLS] 阿里云日志服务客户端初始化成功');
return client;
} catch (error) {
console.error('[SLS] 初始化失败:', error.message);
return null;
}
}
// 刷新日志队列
async function flushLogs() {
if (!client || logQueue.length === 0) return;
const logsToSend = logQueue.splice(0, BATCH_SIZE);
try {
const logItems = logsToSend.map(log => {
const contents = Object.entries(log).map(([key, value]) => {
return new LogContent({
key,
value: String(value ?? '')
});
});
return new LogItem({
time: Math.floor(Date.now() / 1000),
contents
});
});
const request = new PutLogsRequest({
projectName: SLS_CONFIG.project,
logStoreName: SLS_CONFIG.logstore,
logGroup: {
logs: logItems
}
});
await client.putLogs(request);
console.log(`[SLS] 成功上报 ${logsToSend.length} 条日志`);
} catch (error) {
console.error('[SLS] 日志上报失败:', error.message);
// 失败的日志重新入队(可选:限制重试次数)
logQueue.unshift(...logsToSend);
}
}
// 定时刷新
let flushTimer = null;
function startFlushTimer() {
if (flushTimer || !isConfigured) return;
flushTimer = setInterval(flushLogs, FLUSH_INTERVAL_MS);
}
/**
* 记录 API 请求日志
* @param {Object} logData - 日志数据
* @param {string} logData.method - HTTP 方法
* @param {string} logData.endpoint - 请求路径
* @param {string} logData.model - 模型 ID
* @param {number} logData.status - 响应状态码
* @param {number} logData.duration_ms - 请求耗时
* @param {number} [logData.input_tokens] - 输入 Token 数
* @param {number} [logData.output_tokens] - 输出 Token 数
* @param {string} [logData.error] - 错误信息
*/
export function logRequest(logData) {
const enrichedLog = {
timestamp: new Date().toISOString(),
...logData
};
// 始终输出到控制台
console.log('[SLS]', JSON.stringify(enrichedLog));
if (!isConfigured) return;
logQueue.push(enrichedLog);
// 队列满时立即刷新
if (logQueue.length >= BATCH_SIZE) {
flushLogs();
}
}
/**
* 优雅关闭,刷新剩余日志
*/
export async function shutdown() {
if (flushTimer) {
clearInterval(flushTimer);
flushTimer = null;
}
await flushLogs();
console.log('[SLS] 已关闭');
}
// 初始化
initClient();
startFlushTimer();
// 进程退出时优雅关闭
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
export default {
logRequest,
shutdown
};