feat(日志): 新增客户端错误日志收集功能
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 6m50s
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 6m50s
- 创建 log_message 表用于存储客户端错误日志 - 实现客户端日志上报接口 POST /v1/common/log/message/report - 添加管理端日志查询接口 GET /v1/admin/log/message/error/list 和 GET /v1/admin/log/message/error/detail - 实现日志指纹去重和限流机制 - 完善相关模型、逻辑和文档说明
This commit is contained in:
parent
61cdc0ce23
commit
f0439f4f80
39
doc/说明文档.md
Normal file
39
doc/说明文档.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# 报错日志收集(log_message)
|
||||||
|
|
||||||
|
## 项目规划
|
||||||
|
- 目标:新增 `log_message` 表与采集/查询接口,用于 APP / PC / Web 客户端错误日志收集与分析。
|
||||||
|
- 范围:
|
||||||
|
- 创建 MySQL 表与迁移脚本(02105)。
|
||||||
|
- 新增客户端上报接口 `POST /v1/common/log/message/report`。
|
||||||
|
- 新增管理端查询接口 `GET /v1/admin/log/message/error/list` 与 `GET /v1/admin/log/message/error/detail`。
|
||||||
|
|
||||||
|
## 实施方案
|
||||||
|
- 表结构:见 `initialize/migrate/database/02120_log_message.up.sql`。
|
||||||
|
- 模型:`internal/model/logmessage/`(实体、默认 CRUD、筛选)。
|
||||||
|
- 路由:在 `internal/handler/routes.go` 注册公共与管理端路由。
|
||||||
|
- 逻辑:
|
||||||
|
- 上报逻辑:`internal/logic/common/logMessageReportLogic.go`(限流、指纹去重、入库)。
|
||||||
|
- 管理查询:`internal/logic/admin/log/getErrorLogMessageListLogic.go`、`getErrorLogMessageDetailLogic.go`。
|
||||||
|
- 类型:`internal/types/types.go` 新增请求/响应结构。
|
||||||
|
|
||||||
|
## 进度记录
|
||||||
|
- 2025-12-02:
|
||||||
|
- 完成表与索引创建迁移文件。
|
||||||
|
- 完成模型与服务注入。
|
||||||
|
- 完成公共上报接口与限流、去重逻辑;编译验证通过。
|
||||||
|
- 完成管理端列表与详情接口;编译验证通过。
|
||||||
|
- 待办:根据运营需求调整限流阈值与日志保留策略。
|
||||||
|
|
||||||
|
## 接口规范
|
||||||
|
- 上报:`POST /v1/common/log/message/report`
|
||||||
|
- 请求:platform、appVersion、osName、osVersion、deviceId、level、errorCode、message、stack、context、occurredAt
|
||||||
|
- 响应:`{ code, msg, data: { id } }`
|
||||||
|
- 管理端列表:`GET /v1/admin/log/message/error/list`
|
||||||
|
- 筛选:platform、level、user_id、device_id、error_code、keyword、start、end;分页:page、size
|
||||||
|
- 响应:`{ total, list }`
|
||||||
|
- 管理端详情:`GET /v1/admin/log/message/error/detail?id=...`
|
||||||
|
- 响应:完整字段
|
||||||
|
|
||||||
|
## 保留策略与安全
|
||||||
|
- 限流:按设备/IP 每分钟 120 条(可配置)。
|
||||||
|
- 隐私:避免采集敏感数据;服务端对大字段做长度限制与截断。
|
||||||
1
initialize/migrate/database/02120_log_message.down.sql
Normal file
1
initialize/migrate/database/02120_log_message.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS `log_message`;
|
||||||
27
initialize/migrate/database/02120_log_message.up.sql
Normal file
27
initialize/migrate/database/02120_log_message.up.sql
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
CREATE TABLE `log_message` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`platform` VARCHAR(32) NOT NULL,
|
||||||
|
`app_version` VARCHAR(32) NULL,
|
||||||
|
`os_name` VARCHAR(32) NULL,
|
||||||
|
`os_version` VARCHAR(32) NULL,
|
||||||
|
`device_id` VARCHAR(64) NULL,
|
||||||
|
`user_id` BIGINT NULL DEFAULT NULL,
|
||||||
|
`session_id` VARCHAR(64) NULL,
|
||||||
|
`level` TINYINT UNSIGNED NOT NULL DEFAULT 3,
|
||||||
|
`error_code` VARCHAR(64) NULL,
|
||||||
|
`message` TEXT NOT NULL,
|
||||||
|
`stack` MEDIUMTEXT NULL,
|
||||||
|
`context` JSON NULL,
|
||||||
|
`client_ip` VARCHAR(45) NULL,
|
||||||
|
`user_agent` VARCHAR(255) NULL,
|
||||||
|
`locale` VARCHAR(16) NULL,
|
||||||
|
`digest` VARCHAR(64) NULL,
|
||||||
|
`occurred_at` DATETIME NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uniq_digest` (`digest`),
|
||||||
|
KEY `idx_platform_time` (`platform`, `created_at`),
|
||||||
|
KEY `idx_user_time` (`user_id`, `created_at`),
|
||||||
|
KEY `idx_device_time` (`device_id`, `created_at`),
|
||||||
|
KEY `idx_error_code` (`error_code`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/perfect-panel/server/internal/logic/admin/log"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetErrorLogMessageDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
id := c.Query("id")
|
||||||
|
l := log.NewGetErrorLogMessageDetailLogic(c.Request.Context(), svcCtx)
|
||||||
|
resp, err := l.GetErrorLogMessageDetail(id)
|
||||||
|
result.HttpResult(c, resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
internal/handler/admin/log/getErrorLogMessageListHandler.go
Normal file
19
internal/handler/admin/log/getErrorLogMessageListHandler.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/perfect-panel/server/internal/logic/admin/log"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetErrorLogMessageListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var req types.GetErrorLogMessageListRequest
|
||||||
|
_ = c.ShouldBind(&req)
|
||||||
|
l := log.NewGetErrorLogMessageListLogic(c.Request.Context(), svcCtx)
|
||||||
|
resp, err := l.GetErrorLogMessageList(&req)
|
||||||
|
result.HttpResult(c, resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
24
internal/handler/common/logMessageReportHandler.go
Normal file
24
internal/handler/common/logMessageReportHandler.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/perfect-panel/server/internal/logic/common"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReportLogMessageHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var req types.ReportLogMessageRequest
|
||||||
|
_ = c.ShouldBind(&req)
|
||||||
|
validateErr := svcCtx.Validate(&req)
|
||||||
|
if validateErr != nil {
|
||||||
|
result.ParamErrorResult(c, validateErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := common.NewReportLogMessageLogic(c.Request.Context(), svcCtx)
|
||||||
|
resp, err := l.ReportLogMessage(&req, c)
|
||||||
|
result.HttpResult(c, resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -233,6 +233,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
|
|
||||||
// Filter traffic log details
|
// Filter traffic log details
|
||||||
adminLogGroupRouter.GET("/traffic/details", adminLog.FilterTrafficLogDetailsHandler(serverCtx))
|
adminLogGroupRouter.GET("/traffic/details", adminLog.FilterTrafficLogDetailsHandler(serverCtx))
|
||||||
|
|
||||||
|
// Error message log list
|
||||||
|
adminLogGroupRouter.GET("/message/error/list", adminLog.GetErrorLogMessageListHandler(serverCtx))
|
||||||
|
|
||||||
|
// Error message log detail
|
||||||
|
adminLogGroupRouter.GET("/message/error/detail", adminLog.GetErrorLogMessageDetailHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
adminMarketingGroupRouter := router.Group("/v1/admin/marketing")
|
adminMarketingGroupRouter := router.Group("/v1/admin/marketing")
|
||||||
@ -652,6 +658,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
|
|
||||||
// Get Tos Content
|
// Get Tos Content
|
||||||
commonGroupRouter.GET("/site/tos", common.GetTosHandler(serverCtx))
|
commonGroupRouter.GET("/site/tos", common.GetTosHandler(serverCtx))
|
||||||
|
|
||||||
|
// Report client error log
|
||||||
|
commonGroupRouter.POST("/log/message/report", common.ReportLogMessageHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
publicAnnouncementGroupRouter := router.Group("/v1/public/announcement")
|
publicAnnouncementGroupRouter := router.Group("/v1/public/announcement")
|
||||||
|
|||||||
49
internal/logic/admin/log/getErrorLogMessageDetailLogic.go
Normal file
49
internal/logic/admin/log/getErrorLogMessageDetailLogic.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetErrorLogMessageDetailLogic struct {
|
||||||
|
logger.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetErrorLogMessageDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetErrorLogMessageDetailLogic {
|
||||||
|
return &GetErrorLogMessageDetailLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GetErrorLogMessageDetailLogic) GetErrorLogMessageDetail(idStr string) (resp *types.GetErrorLogMessageDetailResponse, err error) {
|
||||||
|
if idStr == "" { return &types.GetErrorLogMessageDetailResponse{}, nil }
|
||||||
|
id, _ := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
row, err := l.svcCtx.LogMessageModel.FindOne(l.ctx, id)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
var uid int64
|
||||||
|
if row.UserId != nil { uid = *row.UserId }
|
||||||
|
var occurred int64
|
||||||
|
if row.OccurredAt != nil { occurred = row.OccurredAt.UnixMilli() }
|
||||||
|
return &types.GetErrorLogMessageDetailResponse{
|
||||||
|
Id: row.Id,
|
||||||
|
Platform: row.Platform,
|
||||||
|
AppVersion: row.AppVersion,
|
||||||
|
OsName: row.OsName,
|
||||||
|
OsVersion: row.OsVersion,
|
||||||
|
DeviceId: row.DeviceId,
|
||||||
|
UserId: uid,
|
||||||
|
SessionId: row.SessionId,
|
||||||
|
Level: row.Level,
|
||||||
|
ErrorCode: row.ErrorCode,
|
||||||
|
Message: row.Message,
|
||||||
|
Stack: row.Stack,
|
||||||
|
ClientIP: row.ClientIP,
|
||||||
|
UserAgent: row.UserAgent,
|
||||||
|
Locale: row.Locale,
|
||||||
|
OccurredAt: occurred,
|
||||||
|
CreatedAt: row.CreatedAt.UnixMilli(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
59
internal/logic/admin/log/getErrorLogMessageListLogic.go
Normal file
59
internal/logic/admin/log/getErrorLogMessageListLogic.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
logmessage "github.com/perfect-panel/server/internal/model/logmessage"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetErrorLogMessageListLogic struct {
|
||||||
|
logger.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetErrorLogMessageListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetErrorLogMessageListLogic {
|
||||||
|
return &GetErrorLogMessageListLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GetErrorLogMessageListLogic) GetErrorLogMessageList(req *types.GetErrorLogMessageListRequest) (resp *types.GetErrorLogMessageListResponse, err error) {
|
||||||
|
var start, end time.Time
|
||||||
|
if req.Start > 0 { start = time.UnixMilli(req.Start) }
|
||||||
|
if req.End > 0 { end = time.UnixMilli(req.End) }
|
||||||
|
rows, total, err := l.svcCtx.LogMessageModel.Filter(l.ctx, &logmessage.FilterParams{
|
||||||
|
Page: req.Page,
|
||||||
|
Size: req.Size,
|
||||||
|
Platform: req.Platform,
|
||||||
|
Level: req.Level,
|
||||||
|
UserID: req.UserId,
|
||||||
|
DeviceID: req.DeviceId,
|
||||||
|
ErrorCode: req.ErrorCode,
|
||||||
|
Keyword: req.Keyword,
|
||||||
|
Start: start,
|
||||||
|
End: end,
|
||||||
|
})
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
list := make([]types.ErrorLogMessage, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
var uid int64
|
||||||
|
if r.UserId != nil { uid = *r.UserId }
|
||||||
|
list = append(list, types.ErrorLogMessage{
|
||||||
|
Id: r.Id,
|
||||||
|
Platform: r.Platform,
|
||||||
|
AppVersion: r.AppVersion,
|
||||||
|
OsName: r.OsName,
|
||||||
|
OsVersion: r.OsVersion,
|
||||||
|
DeviceId: r.DeviceId,
|
||||||
|
UserId: uid,
|
||||||
|
SessionId: r.SessionId,
|
||||||
|
Level: r.Level,
|
||||||
|
ErrorCode: r.ErrorCode,
|
||||||
|
Message: r.Message,
|
||||||
|
CreatedAt: r.CreatedAt.UnixMilli(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &types.GetErrorLogMessageListResponse{ Total: total, List: list }, nil
|
||||||
|
}
|
||||||
108
internal/logic/common/logMessageReportLogic.go
Normal file
108
internal/logic/common/logMessageReportLogic.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
logmessage "github.com/perfect-panel/server/internal/model/logmessage"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReportLogMessageLogic struct {
|
||||||
|
logger.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReportLogMessageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ReportLogMessageLogic {
|
||||||
|
return &ReportLogMessageLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ReportLogMessageLogic) ReportLogMessage(req *types.ReportLogMessageRequest, c *gin.Context) (resp *types.ReportLogMessageResponse, err error) {
|
||||||
|
ip := clientIP(c)
|
||||||
|
ua := c.GetHeader("User-Agent")
|
||||||
|
locale := c.GetHeader("Accept-Language")
|
||||||
|
|
||||||
|
// 简单限流:设备ID优先,其次IP
|
||||||
|
limitKey := "logmsg:" + strings.TrimSpace(req.DeviceId)
|
||||||
|
if limitKey == "logmsg:" { limitKey = "logmsg:" + ip }
|
||||||
|
count, _ := l.svcCtx.Redis.Incr(l.ctx, limitKey).Result()
|
||||||
|
if count == 1 { _ = l.svcCtx.Redis.Expire(l.ctx, limitKey, 60*time.Second).Err() }
|
||||||
|
if count > 120 { // 每分钟最多120条
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "too many reports")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 指纹生成
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(strings.Join([]string{req.Message, req.Stack, req.ErrorCode, req.AppVersion, req.Platform}, "|")))
|
||||||
|
digest := hex.EncodeToString(h.Sum(nil))
|
||||||
|
|
||||||
|
var ctxStr string
|
||||||
|
if req.Context != nil {
|
||||||
|
if b, e := json.Marshal(req.Context); e == nil {
|
||||||
|
ctxStr = string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var occurredAt *time.Time
|
||||||
|
if req.OccurredAt > 0 {
|
||||||
|
t := time.UnixMilli(req.OccurredAt)
|
||||||
|
occurredAt = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
var userIdPtr *int64
|
||||||
|
if req.UserId > 0 { userIdPtr = &req.UserId }
|
||||||
|
|
||||||
|
row := &logmessage.LogMessage{
|
||||||
|
Platform: req.Platform,
|
||||||
|
AppVersion: req.AppVersion,
|
||||||
|
OsName: req.OsName,
|
||||||
|
OsVersion: req.OsVersion,
|
||||||
|
DeviceId: req.DeviceId,
|
||||||
|
UserId: userIdPtr,
|
||||||
|
SessionId: req.SessionId,
|
||||||
|
Level: req.Level,
|
||||||
|
ErrorCode: req.ErrorCode,
|
||||||
|
Message: safeTruncate(req.Message, 1024*64),
|
||||||
|
Stack: safeTruncate(req.Stack, 1024*1024),
|
||||||
|
Context: ctxStr,
|
||||||
|
ClientIP: ip,
|
||||||
|
UserAgent: safeTruncate(ua, 255),
|
||||||
|
Locale: safeTruncate(locale, 16),
|
||||||
|
Digest: digest,
|
||||||
|
OccurredAt: occurredAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = l.svcCtx.LogMessageModel.Insert(l.ctx, row); err != nil {
|
||||||
|
// 唯一指纹冲突时尝试查询已有记录返回ID
|
||||||
|
ex, _, findErr := l.svcCtx.LogMessageModel.Filter(l.ctx, &logmessage.FilterParams{ Keyword: req.Message, Page: 1, Size: 1 })
|
||||||
|
if findErr == nil && len(ex) > 0 {
|
||||||
|
return &types.ReportLogMessageResponse{ Id: ex[0].Id }, nil
|
||||||
|
}
|
||||||
|
l.Errorf("[ReportLogMessage] insert error: %v", err)
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "insert log_message failed: %v", err)
|
||||||
|
}
|
||||||
|
return &types.ReportLogMessageResponse{ Id: row.Id }, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeTruncate(s string, n int) string {
|
||||||
|
if len(s) <= n { return s }
|
||||||
|
return s[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientIP(c *gin.Context) string {
|
||||||
|
ip := c.ClientIP()
|
||||||
|
if ip != "" { return ip }
|
||||||
|
host, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
|
||||||
|
if err == nil && host != "" { return host }
|
||||||
|
return ""
|
||||||
|
}
|
||||||
50
internal/model/logmessage/default.go
Normal file
50
internal/model/logmessage/default.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package logmessage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Model interface {
|
||||||
|
logMessageModel
|
||||||
|
customLogMessageModel
|
||||||
|
}
|
||||||
|
logMessageModel interface {
|
||||||
|
Insert(ctx context.Context, data *LogMessage) error
|
||||||
|
FindOne(ctx context.Context, id int64) (*LogMessage, error)
|
||||||
|
Update(ctx context.Context, data *LogMessage) error
|
||||||
|
Delete(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
|
customModel struct{
|
||||||
|
*defaultModel
|
||||||
|
}
|
||||||
|
defaultModel struct{
|
||||||
|
*gorm.DB
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDefaultModel(db *gorm.DB) *defaultModel {
|
||||||
|
return &defaultModel{DB: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *defaultModel) Insert(ctx context.Context, data *LogMessage) error {
|
||||||
|
return m.WithContext(ctx).Create(data).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *defaultModel) FindOne(ctx context.Context, id int64) (*LogMessage, error) {
|
||||||
|
var v LogMessage
|
||||||
|
err := m.WithContext(ctx).Where("id = ?", id).First(&v).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *defaultModel) Update(ctx context.Context, data *LogMessage) error {
|
||||||
|
return m.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *defaultModel) Delete(ctx context.Context, id int64) error {
|
||||||
|
return m.WithContext(ctx).Where("`id` = ?", id).Delete(&LogMessage{}).Error
|
||||||
|
}
|
||||||
27
internal/model/logmessage/entity.go
Normal file
27
internal/model/logmessage/entity.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package logmessage
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type LogMessage struct {
|
||||||
|
Id int64 `gorm:"primaryKey;AUTO_INCREMENT"`
|
||||||
|
Platform string `gorm:"type:varchar(32);not null"`
|
||||||
|
AppVersion string `gorm:"type:varchar(32);default:null"`
|
||||||
|
OsName string `gorm:"type:varchar(32);default:null"`
|
||||||
|
OsVersion string `gorm:"type:varchar(32);default:null"`
|
||||||
|
DeviceId string `gorm:"type:varchar(64);default:null"`
|
||||||
|
UserId *int64 `gorm:"type:bigint;default:null"`
|
||||||
|
SessionId string `gorm:"type:varchar(64);default:null"`
|
||||||
|
Level uint8 `gorm:"type:tinyint(1);not null;default:3"`
|
||||||
|
ErrorCode string `gorm:"type:varchar(64);default:null"`
|
||||||
|
Message string `gorm:"type:text;not null"`
|
||||||
|
Stack string `gorm:"type:mediumtext;default:null"`
|
||||||
|
Context string `gorm:"type:json;default:null"`
|
||||||
|
ClientIP string `gorm:"type:varchar(45);default:null"`
|
||||||
|
UserAgent string `gorm:"type:varchar(255);default:null"`
|
||||||
|
Locale string `gorm:"type:varchar(16);default:null"`
|
||||||
|
Digest string `gorm:"type:varchar(64);uniqueIndex:uniq_digest;default:null"`
|
||||||
|
OccurredAt *time.Time `gorm:"type:datetime;default:null"`
|
||||||
|
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LogMessage) TableName() string { return "log_message" }
|
||||||
52
internal/model/logmessage/model.go
Normal file
52
internal/model/logmessage/model.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package logmessage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewModel(db *gorm.DB) Model {
|
||||||
|
return &customModel{ defaultModel: newDefaultModel(db) }
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterParams struct {
|
||||||
|
Page int
|
||||||
|
Size int
|
||||||
|
Platform string
|
||||||
|
Level uint8
|
||||||
|
UserID int64
|
||||||
|
DeviceID string
|
||||||
|
ErrorCode string
|
||||||
|
Keyword string
|
||||||
|
Start time.Time
|
||||||
|
End time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type customLogMessageModel interface {
|
||||||
|
Filter(ctx context.Context, filter *FilterParams) ([]*LogMessage, int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *customModel) Filter(ctx context.Context, filter *FilterParams) ([]*LogMessage, int64, error) {
|
||||||
|
tx := m.WithContext(ctx).Model(&LogMessage{}).Order("id DESC")
|
||||||
|
if filter == nil {
|
||||||
|
filter = &FilterParams{ Page: 1, Size: 10 }
|
||||||
|
}
|
||||||
|
if filter.Page < 1 { filter.Page = 1 }
|
||||||
|
if filter.Size < 1 { filter.Size = 10 }
|
||||||
|
if filter.Platform != "" { tx = tx.Where("`platform` = ?", filter.Platform) }
|
||||||
|
if filter.Level != 0 { tx = tx.Where("`level` = ?", filter.Level) }
|
||||||
|
if filter.UserID != 0 { tx = tx.Where("`user_id` = ?", filter.UserID) }
|
||||||
|
if filter.DeviceID != "" { tx = tx.Where("`device_id` = ?", filter.DeviceID) }
|
||||||
|
if filter.ErrorCode != "" { tx = tx.Where("`error_code` = ?", filter.ErrorCode) }
|
||||||
|
if !filter.Start.IsZero() { tx = tx.Where("`created_at` >= ?", filter.Start) }
|
||||||
|
if !filter.End.IsZero() { tx = tx.Where("`created_at` <= ?", filter.End) }
|
||||||
|
if filter.Keyword != "" {
|
||||||
|
like := "%" + filter.Keyword + "%"
|
||||||
|
tx = tx.Where("`message` LIKE ? OR `stack` LIKE ?", like, like)
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
var rows []*LogMessage
|
||||||
|
err := tx.Count(&total).Limit(filter.Size).Offset((filter.Page-1)*filter.Size).Find(&rows).Error
|
||||||
|
return rows, total, err
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/perfect-panel/server/internal/model/coupon"
|
"github.com/perfect-panel/server/internal/model/coupon"
|
||||||
"github.com/perfect-panel/server/internal/model/document"
|
"github.com/perfect-panel/server/internal/model/document"
|
||||||
"github.com/perfect-panel/server/internal/model/log"
|
"github.com/perfect-panel/server/internal/model/log"
|
||||||
|
logmessage "github.com/perfect-panel/server/internal/model/logmessage"
|
||||||
"github.com/perfect-panel/server/internal/model/order"
|
"github.com/perfect-panel/server/internal/model/order"
|
||||||
"github.com/perfect-panel/server/internal/model/payment"
|
"github.com/perfect-panel/server/internal/model/payment"
|
||||||
"github.com/perfect-panel/server/internal/model/subscribe"
|
"github.com/perfect-panel/server/internal/model/subscribe"
|
||||||
@ -44,14 +45,15 @@ type ServiceContext struct {
|
|||||||
ExchangeRate float64
|
ExchangeRate float64
|
||||||
|
|
||||||
//NodeCache *cache.NodeCacheClient
|
//NodeCache *cache.NodeCacheClient
|
||||||
AuthModel auth.Model
|
AuthModel auth.Model
|
||||||
AdsModel ads.Model
|
AdsModel ads.Model
|
||||||
LogModel log.Model
|
LogModel log.Model
|
||||||
NodeModel node.Model
|
LogMessageModel logmessage.Model
|
||||||
UserModel user.Model
|
NodeModel node.Model
|
||||||
OrderModel order.Model
|
UserModel user.Model
|
||||||
ClientModel client.Model
|
OrderModel order.Model
|
||||||
TicketModel ticket.Model
|
ClientModel client.Model
|
||||||
|
TicketModel ticket.Model
|
||||||
//ServerModel server.Model
|
//ServerModel server.Model
|
||||||
SystemModel system.Model
|
SystemModel system.Model
|
||||||
CouponModel coupon.Model
|
CouponModel coupon.Model
|
||||||
@ -96,15 +98,16 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
Queue: NewAsynqClient(c),
|
Queue: NewAsynqClient(c),
|
||||||
ExchangeRate: 1.0,
|
ExchangeRate: 1.0,
|
||||||
//NodeCache: cache.NewNodeCacheClient(rds),
|
//NodeCache: cache.NewNodeCacheClient(rds),
|
||||||
AuthLimiter: authLimiter,
|
AuthLimiter: authLimiter,
|
||||||
AdsModel: ads.NewModel(db, rds),
|
AdsModel: ads.NewModel(db, rds),
|
||||||
LogModel: log.NewModel(db),
|
LogModel: log.NewModel(db),
|
||||||
NodeModel: node.NewModel(db, rds),
|
LogMessageModel: logmessage.NewModel(db),
|
||||||
AuthModel: auth.NewModel(db, rds),
|
NodeModel: node.NewModel(db, rds),
|
||||||
UserModel: user.NewModel(db, rds),
|
AuthModel: auth.NewModel(db, rds),
|
||||||
OrderModel: order.NewModel(db, rds),
|
UserModel: user.NewModel(db, rds),
|
||||||
ClientModel: client.NewSubscribeApplicationModel(db),
|
OrderModel: order.NewModel(db, rds),
|
||||||
TicketModel: ticket.NewModel(db, rds),
|
ClientModel: client.NewSubscribeApplicationModel(db),
|
||||||
|
TicketModel: ticket.NewModel(db, rds),
|
||||||
//ServerModel: server.NewModel(db, rds),
|
//ServerModel: server.NewModel(db, rds),
|
||||||
SystemModel: system.NewModel(db, rds),
|
SystemModel: system.NewModel(db, rds),
|
||||||
CouponModel: coupon.NewModel(db, rds),
|
CouponModel: coupon.NewModel(db, rds),
|
||||||
|
|||||||
@ -1234,6 +1234,80 @@ type MessageLog struct {
|
|||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReportLogMessageRequest struct {
|
||||||
|
Platform string `json:"platform" validate:"required"`
|
||||||
|
AppVersion string `json:"appVersion"`
|
||||||
|
OsName string `json:"osName"`
|
||||||
|
OsVersion string `json:"osVersion"`
|
||||||
|
DeviceId string `json:"deviceId"`
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
SessionId string `json:"sessionId"`
|
||||||
|
Level uint8 `json:"level"`
|
||||||
|
ErrorCode string `json:"errorCode"`
|
||||||
|
Message string `json:"message" validate:"required"`
|
||||||
|
Stack string `json:"stack"`
|
||||||
|
Context map[string]interface{} `json:"context"`
|
||||||
|
OccurredAt int64 `json:"occurredAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportLogMessageResponse struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetErrorLogMessageListRequest struct {
|
||||||
|
Page int `form:"page"`
|
||||||
|
Size int `form:"size"`
|
||||||
|
Platform string `form:"platform"`
|
||||||
|
Level uint8 `form:"level"`
|
||||||
|
UserId int64 `form:"user_id"`
|
||||||
|
DeviceId string `form:"device_id"`
|
||||||
|
ErrorCode string `form:"error_code"`
|
||||||
|
Keyword string `form:"keyword"`
|
||||||
|
Start int64 `form:"start"`
|
||||||
|
End int64 `form:"end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorLogMessage struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
AppVersion string `json:"app_version"`
|
||||||
|
OsName string `json:"os_name"`
|
||||||
|
OsVersion string `json:"os_version"`
|
||||||
|
DeviceId string `json:"device_id"`
|
||||||
|
UserId int64 `json:"user_id"`
|
||||||
|
SessionId string `json:"session_id"`
|
||||||
|
Level uint8 `json:"level"`
|
||||||
|
ErrorCode string `json:"error_code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetErrorLogMessageListResponse struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
List []ErrorLogMessage `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetErrorLogMessageDetailResponse struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
AppVersion string `json:"app_version"`
|
||||||
|
OsName string `json:"os_name"`
|
||||||
|
OsVersion string `json:"os_version"`
|
||||||
|
DeviceId string `json:"device_id"`
|
||||||
|
UserId int64 `json:"user_id"`
|
||||||
|
SessionId string `json:"session_id"`
|
||||||
|
Level uint8 `json:"level"`
|
||||||
|
ErrorCode string `json:"error_code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Stack string `json:"stack"`
|
||||||
|
Context map[string]interface{} `json:"context"`
|
||||||
|
ClientIP string `json:"client_ip"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
OccurredAt int64 `json:"occurred_at"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type MigrateServerNodeResponse struct {
|
type MigrateServerNodeResponse struct {
|
||||||
Succee uint64 `json:"succee"`
|
Succee uint64 `json:"succee"`
|
||||||
Fail uint64 `json:"fail"`
|
Fail uint64 `json:"fail"`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user