feat: add error logging functionality to relay and update logs table for error type display

This commit is contained in:
jasonzeng
2025-04-12 00:43:34 +08:00
parent ef8ae4db80
commit 97bc2b4474
3 changed files with 124 additions and 71 deletions

View File

@@ -39,6 +39,26 @@ func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
default:
err = relay.TextHelper(c)
}
if err != nil {
// 保存错误日志到mysql中
userId := c.GetInt("id")
tokenName := c.GetString("token_name")
modelName := c.GetString("original_model")
tokenId := c.GetInt("token_id")
userGroup := c.GetString("group")
channelId := c.GetInt("channel_id")
other := make(map[string]interface{})
other["error_type"] = err.Error.Type
other["error_code"] = err.Error.Code
other["status_code"] = err.StatusCode
other["channel_id"] = channelId
other["channel_name"] = c.GetString("channel_name")
other["channel_type"] = c.GetInt("channel_type")
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.Error.Message, tokenId, 0, false, userGroup, other)
}
return err
}

View File

@@ -40,6 +40,7 @@ const (
LogTypeConsume
LogTypeManage
LogTypeSystem
LogTypeError
)
func formatUserLogs(logs []*Log) {
@@ -88,6 +89,35 @@ func RecordLog(userId int, logType int, content string) {
}
}
func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, tokenName string, content string, tokenId int, useTimeSeconds int,
isStream bool, group string, other map[string]interface{}) {
common.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content))
username := c.GetString("username")
otherStr := common.MapToJsonStr(other)
log := &Log{
UserId: userId,
Username: username,
CreatedAt: common.GetTimestamp(),
Type: LogTypeError,
Content: content,
PromptTokens: 0,
CompletionTokens: 0,
TokenName: tokenName,
ModelName: modelName,
Quota: 0,
ChannelId: channelId,
TokenId: tokenId,
UseTime: useTimeSeconds,
IsStream: isStream,
Group: group,
Other: otherStr,
}
err := LOG_DB.Create(log).Error
if err != nil {
common.LogError(c, "failed to record log: "+err.Error())
}
}
func RecordConsumeLog(c *gin.Context, userId int, channelId int, promptTokens int, completionTokens int,
modelName string, tokenName string, quota int, content string, tokenId int, userQuota int, useTimeSeconds int,
isStream bool, group string, other map[string]interface{}) {

View File

@@ -85,8 +85,10 @@ const LogsTable = () => {
return <Tag color='orange' size='large'>{t('管理')}</Tag>;
case 4:
return <Tag color='purple' size='large'>{t('系统')}</Tag>;
case 5:
return <Tag color='red' size='large'>{t('错误')}</Tag>;
default:
return <Tag color='black' size='large'>{t('未知')}</Tag>;
return <Tag color='grey' size='large'>{t('未知')}</Tag>;
}
}
@@ -160,7 +162,7 @@ const LogsTable = () => {
color={stringToColor(record.model_name)}
size='large'
onClick={(event) => {
copyText(event, record.model_name).then(r => {});
copyText(event, record.model_name).then(r => { });
}}
>
{' '}{record.model_name}{' '}
@@ -170,13 +172,13 @@ const LogsTable = () => {
<>
<Space vertical align={'start'}>
<Popover content={
<div style={{padding: 10}}>
<div style={{ padding: 10 }}>
<Space vertical align={'start'}>
<Tag
color={stringToColor(record.model_name)}
size='large'
onClick={(event) => {
copyText(event, record.model_name).then(r => {});
copyText(event, record.model_name).then(r => { });
}}
>
{t('请求并计费模型')}{' '}{record.model_name}{' '}
@@ -185,7 +187,7 @@ const LogsTable = () => {
color={stringToColor(other.upstream_model_name)}
size='large'
onClick={(event) => {
copyText(event, other.upstream_model_name).then(r => {});
copyText(event, other.upstream_model_name).then(r => { });
}}
>
{t('实际模型')}{' '}{other.upstream_model_name}{' '}
@@ -197,9 +199,9 @@ const LogsTable = () => {
color={stringToColor(record.model_name)}
size='large'
onClick={(event) => {
copyText(event, record.model_name).then(r => {});
copyText(event, record.model_name).then(r => { });
}}
suffixIcon={<IconRefresh style={{width: '0.8em', height: '0.8em', opacity: 0.6}} />}
suffixIcon={<IconRefresh style={{ width: '0.8em', height: '0.8em', opacity: 0.6 }} />}
>
{' '}{record.model_name}{' '}
</Tag>
@@ -298,7 +300,7 @@ const LogsTable = () => {
const handleSelectAll = (checked) => {
const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]);
const updatedColumns = {};
allKeys.forEach(key => {
// For admin-only columns, only enable them if user is admin
if ((key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.USERNAME || key === COLUMN_KEYS.RETRY) && !isAdminUser) {
@@ -307,7 +309,7 @@ const LogsTable = () => {
updatedColumns[key] = checked;
}
});
setVisibleColumns(updatedColumns);
};
@@ -325,7 +327,7 @@ const LogsTable = () => {
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return isAdminUser ? (
record.type === 0 || record.type === 2 ? (
(record.type === 0 || record.type === 2 || record.type === 5) ? (
<div>
{
<Tooltip content={record.channel_name || '[未知]'}>
@@ -378,7 +380,7 @@ const LogsTable = () => {
title: t('令牌'),
dataIndex: 'token_name',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
return (record.type === 0 || record.type === 2 || record.type === 5) ? (
<div>
<Tag
color='grey'
@@ -402,33 +404,33 @@ const LogsTable = () => {
title: t('分组'),
dataIndex: 'group',
render: (text, record, index) => {
if (record.type === 0 || record.type === 2) {
if (record.group) {
if (record.type === 0 || record.type === 2 || record.type === 5) {
if (record.group) {
return (
<>
{renderGroup(record.group)}
</>
);
} else {
let other = null;
try {
other = JSON.parse(record.other);
} catch (e) {
console.error(`Failed to parse record.other: "${record.other}".`, e);
}
if (other === null) {
return <></>;
}
if (other.group !== undefined) {
return (
<>
{renderGroup(other.group)}
</>
);
} else {
return <></>;
}
}
} else {
let other = null;
try {
other = JSON.parse(record.other);
} catch (e) {
console.error(`Failed to parse record.other: "${record.other}".`, e);
}
if (other === null) {
return <></>;
}
if (other.group !== undefined) {
return (
<>
{renderGroup(other.group)}
</>
);
} else {
return <></>;
}
}
} else {
return <></>;
}
@@ -447,7 +449,7 @@ const LogsTable = () => {
title: t('模型'),
dataIndex: 'model_name',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
return (record.type === 0 || record.type === 2 || record.type === 5) ? (
<>{renderModelName(record)}</>
) : (
<></>
@@ -487,7 +489,7 @@ const LogsTable = () => {
title: t('提示'),
dataIndex: 'prompt_tokens',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
return (record.type === 0 || record.type === 2 || record.type === 5) ? (
<>{<span> {text} </span>}</>
) : (
<></>
@@ -500,7 +502,7 @@ const LogsTable = () => {
dataIndex: 'completion_tokens',
render: (text, record, index) => {
return parseInt(text) > 0 &&
(record.type === 0 || record.type === 2) ? (
(record.type === 0 || record.type === 2 || record.type === 5) ? (
<>{<span> {text} </span>}</>
) : (
<></>
@@ -512,7 +514,7 @@ const LogsTable = () => {
title: t('花费'),
dataIndex: 'quota',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
return (record.type === 0 || record.type === 2 || record.type === 5) ? (
<>{renderQuota(text, 6)}</>
) : (
<></>
@@ -588,14 +590,14 @@ const LogsTable = () => {
other.cache_ratio || 1.0,
);
return (
<Paragraph
ellipsis={{
rows: 2,
}}
style={{ maxWidth: 240 }}
>
{content}
</Paragraph>
<Paragraph
ellipsis={{
rows: 2,
}}
style={{ maxWidth: 240 }}
>
{content}
</Paragraph>
);
},
},
@@ -638,8 +640,8 @@ const LogsTable = () => {
{t('全选')}
</Checkbox>
</div>
<div style={{
display: 'flex',
<div style={{
display: 'flex',
flexWrap: 'wrap',
maxHeight: '400px',
overflowY: 'auto',
@@ -649,12 +651,12 @@ const LogsTable = () => {
}}>
{allColumns.map(column => {
// Skip admin-only columns for non-admin users
if (!isAdminUser && (column.key === COLUMN_KEYS.CHANNEL ||
column.key === COLUMN_KEYS.USERNAME ||
column.key === COLUMN_KEYS.RETRY)) {
if (!isAdminUser && (column.key === COLUMN_KEYS.CHANNEL ||
column.key === COLUMN_KEYS.USERNAME ||
column.key === COLUMN_KEYS.RETRY)) {
return null;
}
return (
<div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}>
<Checkbox
@@ -803,7 +805,7 @@ const LogsTable = () => {
// key: '渠道重试',
// value: content,
// })
}
}
if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
expandDataLocal.push({
key: t('渠道信息'),
@@ -962,7 +964,7 @@ const LogsTable = () => {
const handlePageChange = (page) => {
setActivePage(page);
loadLogs(page, pageSize, logType).then((r) => {});
loadLogs(page, pageSize, logType).then((r) => { });
};
const handlePageSizeChange = async (size) => {
@@ -1014,26 +1016,26 @@ const LogsTable = () => {
<Header>
<Spin spinning={loadingStat}>
<Space>
<Tag color='blue' size='large' style={{
padding: 15,
borderRadius: '8px',
<Tag color='blue' size='large' style={{
padding: 15,
borderRadius: '8px',
fontWeight: 500,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)'
}}>
{t('消耗额度')}: {renderQuota(stat.quota)}
</Tag>
<Tag color='pink' size='large' style={{
padding: 15,
borderRadius: '8px',
<Tag color='pink' size='large' style={{
padding: 15,
borderRadius: '8px',
fontWeight: 500,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)'
}}>
RPM: {stat.rpm}
</Tag>
<Tag color='white' size='large' style={{
padding: 15,
border: 'none',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
<Tag color='white' size='large' style={{
padding: 15,
border: 'none',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
borderRadius: '8px',
fontWeight: 500,
}}>
@@ -1046,7 +1048,7 @@ const LogsTable = () => {
<>
<Form.Section>
<div style={{ marginBottom: 10 }}>
{
{
styleState.isMobile ? (
<div>
<Form.DatePicker
@@ -1146,20 +1148,21 @@ const LogsTable = () => {
<Form.Section></Form.Section>
</>
</Form>
<div style={{marginTop:10}}>
<div style={{ marginTop: 10 }}>
<Select
defaultValue='0'
style={{ width: 120 }}
onChange={(value) => {
setLogType(parseInt(value));
loadLogs(0, pageSize, parseInt(value));
}}
defaultValue='0'
style={{ width: 120 }}
onChange={(value) => {
setLogType(parseInt(value));
loadLogs(0, pageSize, parseInt(value));
}}
>
<Select.Option value='0'>{t('全部')}</Select.Option>
<Select.Option value='1'>{t('充值')}</Select.Option>
<Select.Option value='2'>{t('消费')}</Select.Option>
<Select.Option value='3'>{t('管理')}</Select.Option>
<Select.Option value='4'>{t('系统')}</Select.Option>
<Select.Option value='5'>{t('错误')}</Select.Option>
</Select>
<Button
theme='light'