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
|
||||
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")
|
||||
@ -652,6 +658,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Get Tos Content
|
||||
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")
|
||||
|
||||
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/document"
|
||||
"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/payment"
|
||||
"github.com/perfect-panel/server/internal/model/subscribe"
|
||||
@ -44,14 +45,15 @@ type ServiceContext struct {
|
||||
ExchangeRate float64
|
||||
|
||||
//NodeCache *cache.NodeCacheClient
|
||||
AuthModel auth.Model
|
||||
AdsModel ads.Model
|
||||
LogModel log.Model
|
||||
NodeModel node.Model
|
||||
UserModel user.Model
|
||||
OrderModel order.Model
|
||||
ClientModel client.Model
|
||||
TicketModel ticket.Model
|
||||
AuthModel auth.Model
|
||||
AdsModel ads.Model
|
||||
LogModel log.Model
|
||||
LogMessageModel logmessage.Model
|
||||
NodeModel node.Model
|
||||
UserModel user.Model
|
||||
OrderModel order.Model
|
||||
ClientModel client.Model
|
||||
TicketModel ticket.Model
|
||||
//ServerModel server.Model
|
||||
SystemModel system.Model
|
||||
CouponModel coupon.Model
|
||||
@ -96,15 +98,16 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
Queue: NewAsynqClient(c),
|
||||
ExchangeRate: 1.0,
|
||||
//NodeCache: cache.NewNodeCacheClient(rds),
|
||||
AuthLimiter: authLimiter,
|
||||
AdsModel: ads.NewModel(db, rds),
|
||||
LogModel: log.NewModel(db),
|
||||
NodeModel: node.NewModel(db, rds),
|
||||
AuthModel: auth.NewModel(db, rds),
|
||||
UserModel: user.NewModel(db, rds),
|
||||
OrderModel: order.NewModel(db, rds),
|
||||
ClientModel: client.NewSubscribeApplicationModel(db),
|
||||
TicketModel: ticket.NewModel(db, rds),
|
||||
AuthLimiter: authLimiter,
|
||||
AdsModel: ads.NewModel(db, rds),
|
||||
LogModel: log.NewModel(db),
|
||||
LogMessageModel: logmessage.NewModel(db),
|
||||
NodeModel: node.NewModel(db, rds),
|
||||
AuthModel: auth.NewModel(db, rds),
|
||||
UserModel: user.NewModel(db, rds),
|
||||
OrderModel: order.NewModel(db, rds),
|
||||
ClientModel: client.NewSubscribeApplicationModel(db),
|
||||
TicketModel: ticket.NewModel(db, rds),
|
||||
//ServerModel: server.NewModel(db, rds),
|
||||
SystemModel: system.NewModel(db, rds),
|
||||
CouponModel: coupon.NewModel(db, rds),
|
||||
|
||||
@ -1234,6 +1234,80 @@ type MessageLog struct {
|
||||
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 {
|
||||
Succee uint64 `json:"succee"`
|
||||
Fail uint64 `json:"fail"`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user