优化:运行日志业务语义与可视化展示改造,去除内部协议干扰并统一中文呈现
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-11 21:18:31 +08:00
parent aec0d23ac0
commit 12475f5565
7 changed files with 175 additions and 15 deletions

View File

@ -1,6 +1,7 @@
package config
import (
"regexp"
"strconv"
"strings"
@ -8,6 +9,50 @@ import (
log "github.com/sirupsen/logrus"
)
var knownActionMap = map[string]string{
"imap_server.(*serverSession).Login": "IMAP 客户端登录",
"imap_server.(*serverSession).Close": "IMAP 断开连接",
"imap_server.(*serverSession).Select": "IMAP 选择邮箱",
"imap_server.(*serverSession).Fetch": "IMAP 获取邮件",
"imap_server.(*serverSession).Append": "IMAP 追加邮件",
"imap_server.(*serverSession).Delete": "IMAP 删除邮箱",
"pop3_server.action.Pass": "POP3 客户端登录",
"pop3_server.action.Retr": "POP3 获取邮件",
"smtp_server.(*Session).AuthPlain": "SMTP 客户端认证",
"send.doSend": "SMTP 邮件投递",
"send.doSend.func2": "SMTP 邮件投递",
"send.Send": "发送邮件",
}
var cleanFuncRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)\.(?:\(\*?[a-zA-Z0-9_]+\)\.)?([a-zA-Z0-9_]+)(?:\.func\d+)?$`)
func friendlyActionFromCaller(function string) string {
if action, ok := knownActionMap[function]; ok {
return action
}
m := cleanFuncRe.FindStringSubmatch(function)
if len(m) == 3 {
pkg := m[1]
fn := m[2]
pkgLabel := map[string]string{
"imap_server": "IMAP 服务",
"pop3_server": "POP3 服务",
"smtp_server": "SMTP 服务",
"send": "邮件投递",
"controllers": "接口处理",
"services": "系统服务",
"res_init": "系统初始化",
"hooks": "插件钩子",
"adminreset": "超级管理",
}[pkg]
if pkgLabel == "" {
pkgLabel = pkg
}
return pkgLabel + " · " + fn
}
return "运行事件 · " + function
}
// runtimeEventFields 为文件日志提供稳定的事件边界;没有完成信号时只记为状态记录。
// 不从任意消息中的“成功”或日志级别猜测业务结果,也不改变调用方的字段。
func runtimeEventFields(entry *log.Entry) map[string]any {
@ -30,7 +75,7 @@ func runtimeEventFields(entry *log.Entry) map[string]any {
if index := strings.LastIndex(function, "/"); index >= 0 {
function = function[index+1:]
}
action += " · " + function
action = friendlyActionFromCaller(function)
}
fields["action"] = action
}
@ -38,4 +83,4 @@ func runtimeEventFields(entry *log.Entry) map[string]any {
fields["result"] = "recorded"
}
return fields
}
}

View File

@ -10,7 +10,11 @@ import (
func (s *serverSession) Login(username, pwd string) error {
user, err := auth.AuthenticateProtocol("imap", s.remoteAddress, username, pwd)
if err != nil {
log.WithContext(s.ctx).Info("IMAP authentication failed")
log.WithContext(s.ctx).WithFields(log.Fields{
"action": "IMAP 客户端登录",
"result": "failure",
"object": s.remoteAddress,
}).Info("IMAP 客户端认证失败")
return &imap.Error{
Type: imap.StatusResponseTypeNo,
Code: imap.ResponseCodeAuthenticationFailed,
@ -24,6 +28,10 @@ func (s *serverSession) Login(username, pwd string) error {
s.ctx.UserAccount = user.Account
s.ctx.Role = user.EffectiveRole()
s.ctx.IsAdmin = s.ctx.Role == access.RoleSuperAdmin || s.ctx.Role == access.RoleAdmin
log.WithContext(s.ctx).Debug("IMAP authentication succeeded")
log.WithContext(s.ctx).WithFields(log.Fields{
"action": "IMAP 客户端登录",
"result": "success",
"object": s.remoteAddress,
}).Debug("IMAP 客户端认证成功")
return nil
}
}

View File

@ -105,7 +105,11 @@ func (a action) Pass(session *gopop.Session, pwd string) error {
}
user, err := auth.AuthenticateProtocol("pop3", remoteAddress, session.User, pwd)
if err != nil {
log.WithContext(session.Ctx).Info("POP3 authentication failed")
log.WithContext(session.Ctx).WithFields(log.Fields{
"action": "POP3 客户端登录",
"result": "failure",
"object": remoteAddress,
}).Info("POP3 客户端认证失败")
return errors2.New("password error")
}

View File

@ -64,7 +64,11 @@ func (s *Session) Auth(mech string) (sasl.Server, error) {
func (s *Session) AuthPlain(username, pwd string) error {
user, err := auth.AuthenticateProtocol("smtp", s.RemoteAddress.String(), username, pwd)
if err != nil {
log.WithContext(s.Ctx).Info("SMTP authentication failed")
log.WithContext(s.Ctx).WithFields(log.Fields{
"action": "SMTP 客户端认证",
"result": "failure",
"object": s.RemoteAddress.String(),
}).Info("SMTP 客户端认证失败")
return errors.New("password error")
}

View File

@ -277,7 +277,11 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai
recordFailure(err)
return
}
log.WithContext(ctx).Infof("SMTP STARTTLS on 25 Send Error. %s", err.Error())
log.WithContext(ctx).WithFields(log.Fields{
"action": "SMTP 邮件投递",
"result": "failure",
"object": domain.mxHost + ":25",
}).Infof("SMTP 25 端口 STARTTLS 投递失败: %s", err.Error())
// 再试用587投递
err = smtp.SendMailWithTls(strings.TrimSuffix(domain.mxHost, "."), domain.mxHost+":587", nil, from, fromDomain, buildAddress(tos), data)
@ -293,7 +297,11 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai
recordFailure(err)
return
}
log.WithContext(ctx).Infof("SMTPS on 587 Send Error. %s", err.Error())
log.WithContext(ctx).WithFields(log.Fields{
"action": "SMTP 邮件投递",
"result": "failure",
"object": domain.mxHost + ":587",
}).Infof("SMTP 587 端口投递失败: %s", err.Error())
// 再次尝试465投递
err = smtp.SendMailWithTls(strings.TrimSuffix(domain.mxHost, "."), domain.mxHost+":465", nil, from, fromDomain, buildAddress(tos), data)
@ -309,7 +317,11 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai
recordFailure(err)
return
}
log.WithContext(ctx).Infof("SMTPS on 465 Send Error. %s", err.Error())
log.WithContext(ctx).WithFields(log.Fields{
"action": "SMTP 邮件投递",
"result": "failure",
"object": domain.mxHost + ":465",
}).Infof("SMTP 465 端口投递失败: %s", err.Error())
// 所有安全投递方式均失败后记录错误,不允许回退到明文连接。
recordFailure(err)

View File

@ -179,4 +179,4 @@ const {
} = useLogs()
</script>
<style scoped src="./LogSettings.css"></style>
<style scoped src="./LogSettings.css"></style>

View File

@ -22,6 +22,86 @@ export function timeParts(value) {
return { date: localDate.format(date), time: localTime.format(date) }
}
export function cleanRuntimeMessage(text) {
if (!text) return ''
const boundary = text.lastIndexOf(' @event ')
if (boundary >= 0) {
return text.slice(0, boundary).trim()
}
return text
}
export function friendlyOutcomeText(text) {
if (!text) return '(无详细说明)'
const trimmed = text.trim()
const dictionary = {
'IMAP authentication failed': 'IMAP 客户端登录认证失败',
'IMAP authentication succeeded': 'IMAP 客户端登录认证成功',
'POP3 authentication failed': 'POP3 客户端登录认证失败',
'POP3 authentication succeeded': 'POP3 客户端登录认证成功',
'SMTP authentication failed': 'SMTP 客户端登录认证失败',
'SMTP authentication succeeded': 'SMTP 客户端登录认证成功',
'SMTP MAIL accepted': 'SMTP 接收发件人地址',
'SMTP RCPT accepted': 'SMTP 接收收件人地址',
'Plugin Stop': '停止插件执行',
'Server Start Success': '服务启动成功',
'开始绑定监听端口': '开始绑定并监听网络端口'
}
if (dictionary[trimmed]) return dictionary[trimmed]
if (/^SMTP STARTTLS on 25 Send Error\.\s*(.*)$/i.test(trimmed)) {
const detail = trimmed.replace(/^SMTP STARTTLS on 25 Send Error\.\s*/i, '')
return `SMTP 25 端口 STARTTLS 投递失败:${detail}`
}
if (/^SMTPS on 587 Send Error\.\s*(.*)$/i.test(trimmed)) {
const detail = trimmed.replace(/^SMTPS on 587 Send Error\.\s*/i, '')
return `SMTP 587 端口投递失败:${detail}`
}
if (/^SMTPS on 465 Send Error\.\s*(.*)$/i.test(trimmed)) {
const detail = trimmed.replace(/^SMTPS on 465 Send Error\.\s*/i, '')
return `SMTP 465 端口投递失败:${detail}`
}
return trimmed
}
export function friendlyActionText(action) {
if (!action) return '历史运行事件'
const known = {
'运行事件 · imap_server.(*serverSession).Login': 'IMAP 客户端登录',
'运行事件 · imap_server.(*serverSession).Close': 'IMAP 断开连接',
'运行事件 · imap_server.(*serverSession).Select': 'IMAP 选择邮箱',
'运行事件 · imap_server.(*serverSession).Fetch': 'IMAP 获取邮件',
'运行事件 · imap_server.(*serverSession).Append': 'IMAP 追加邮件',
'运行事件 · imap_server.(*serverSession).Delete': 'IMAP 删除邮箱',
'运行事件 · pop3_server.action.Pass': 'POP3 客户端登录',
'运行事件 · pop3_server.action.Retr': 'POP3 获取邮件',
'运行事件 · smtp_server.(*Session).AuthPlain': 'SMTP 客户端认证',
'运行事件 · send.doSend': 'SMTP 邮件投递',
'运行事件 · send.doSend.func2': 'SMTP 邮件投递',
'运行事件 · send.Send': '发送邮件'
}
if (known[action]) return known[action]
const m = action.match(/^运行事件\s*·\s*([a-zA-Z0-9_]+)\.(?:\(\*?[a-zA-Z0-9_]+\)\.)?([a-zA-Z0-9_]+)(?:\.func\d+)?$/)
if (m) {
const pkg = m[1]
const fn = m[2]
const pkgLabel = {
imap_server: 'IMAP 服务',
pop3_server: 'POP3 服务',
smtp_server: 'SMTP 服务',
send: '邮件投递',
controllers: '接口处理',
services: '系统服务',
res_init: '系统初始化',
hooks: '插件钩子',
adminreset: '超级管理'
}[pkg] || pkg
return `${pkgLabel} · ${fn}`
}
return action
}
// 只识别已知的日志头;未知格式原样展示,不丢弃记录或猜测级别。
export function parseRuntimeLine(line, sequence) {
const raw = String(line ?? '')
@ -67,10 +147,17 @@ export function parseRuntimeLine(line, sequence) {
// 旧日志或截断记录保留原文,不能猜测结果。
}
}
const displayMessage = cleanRuntimeMessage(message)
const rawAction = event.action || '历史运行事件'
const action = friendlyActionText(rawAction)
const rawOutcome = event.outcome || content
const outcome = friendlyOutcomeText(rawOutcome)
return {
sequence, raw, createdAt, level, source, sourceShort, requestId, message, summary,
action: event.action || '历史运行事件', result: event.result || 'unknown',
object: event.object || event.section || '', outcome: event.outcome || content,
sequence, raw, createdAt, level, source, sourceShort, requestId,
message: displayMessage, summary,
action, result: event.result || 'unknown',
object: event.object || event.section || '', outcome,
...timeParts(createdAt), ...levels[level]
}
}
@ -90,4 +177,4 @@ export function formatFileSize(value) {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
}