feat: 添加请求追踪中间件并支持查询过期订阅
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 5m10s
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 5m10s
添加请求追踪中间件以记录请求和响应内容 在用户订阅查询中新增includeExpired参数支持查询历史记录 完善配置系统以支持float64类型默认值解析
This commit is contained in:
parent
55c778b65b
commit
ef64a876cd
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/orm"
|
"github.com/perfect-panel/server/pkg/orm"
|
||||||
"github.com/perfect-panel/server/pkg/service"
|
"github.com/perfect-panel/server/pkg/service"
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
"github.com/perfect-panel/server/pkg/tool"
|
||||||
|
"github.com/perfect-panel/server/pkg/trace"
|
||||||
"github.com/perfect-panel/server/queue"
|
"github.com/perfect-panel/server/queue"
|
||||||
"github.com/perfect-panel/server/scheduler"
|
"github.com/perfect-panel/server/scheduler"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -49,6 +50,7 @@ var startCmd = &cobra.Command{
|
|||||||
func run() {
|
func run() {
|
||||||
services := getServers()
|
services := getServers()
|
||||||
defer services.Stop()
|
defer services.Stop()
|
||||||
|
defer trace.StopAgent()
|
||||||
go services.Start()
|
go services.Start()
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
@ -89,6 +91,9 @@ func getServers() *service.Group {
|
|||||||
logger.Errorf("Logger setup failed: %v", err.Error())
|
logger.Errorf("Logger setup failed: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init trace
|
||||||
|
trace.StartAgent(c.Trace)
|
||||||
|
|
||||||
// init service context
|
// init service context
|
||||||
ctx := svc.NewServiceContext(c)
|
ctx := svc.NewServiceContext(c)
|
||||||
services := service.NewServiceGroup()
|
services := service.NewServiceGroup()
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
"github.com/perfect-panel/server/pkg/orm"
|
"github.com/perfect-panel/server/pkg/orm"
|
||||||
|
"github.com/perfect-panel/server/pkg/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -29,6 +30,7 @@ type Config struct {
|
|||||||
Invite InviteConfig `yaml:"Invite"`
|
Invite InviteConfig `yaml:"Invite"`
|
||||||
Telegram Telegram `yaml:"Telegram"`
|
Telegram Telegram `yaml:"Telegram"`
|
||||||
Log Log `yaml:"Log"`
|
Log Log `yaml:"Log"`
|
||||||
|
Trace trace.Config `yaml:"Trace"`
|
||||||
Administrator struct {
|
Administrator struct {
|
||||||
Email string `yaml:"Email" default:"admin@ppanel.dev"`
|
Email string `yaml:"Email" default:"admin@ppanel.dev"`
|
||||||
Password string `yaml:"Password" default:"password"`
|
Password string `yaml:"Password" default:"password"`
|
||||||
|
|||||||
@ -1,17 +1,26 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Query User Subscribe
|
// Query User Subscribe
|
||||||
func QueryUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
func QueryUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
// 1. Get param from URL Query (?includeExpired=all)
|
||||||
|
value := c.Query("includeExpired")
|
||||||
|
|
||||||
l := user.NewQueryUserSubscribeLogic(c.Request.Context(), svcCtx)
|
// 2. Inject param into Request Context
|
||||||
|
// Note: Must use context.WithValue to create new ctx
|
||||||
|
ctx := context.WithValue(c.Request.Context(), constant.CtxKeyIncludeExpired, value)
|
||||||
|
|
||||||
|
l := user.NewQueryUserSubscribeLogic(ctx, svcCtx)
|
||||||
resp, err := l.QueryUserSubscribe()
|
resp, err := l.QueryUserSubscribe()
|
||||||
result.HttpResult(c, resp, err)
|
result.HttpResult(c, resp, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||||
|
router.Use(middleware.TraceMiddleware(serverCtx))
|
||||||
|
|
||||||
adminAdsGroupRouter := router.Group("/v1/admin/ads")
|
adminAdsGroupRouter := router.Group("/v1/admin/ads")
|
||||||
adminAdsGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
adminAdsGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -18,6 +20,17 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/trace"
|
"github.com/perfect-panel/server/pkg/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// bodyLogWriter is a wrapper for gin.ResponseWriter to capture response body
|
||||||
|
type bodyLogWriter struct {
|
||||||
|
gin.ResponseWriter
|
||||||
|
body *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w bodyLogWriter) Write(b []byte) (int, error) {
|
||||||
|
w.body.Write(b)
|
||||||
|
return w.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
// statusByWriter returns a span status code and message for an HTTP status code
|
// statusByWriter returns a span status code and message for an HTTP status code
|
||||||
// value returned by a server. Status codes in the 400-499 range are not
|
// value returned by a server. Status codes in the 400-499 range are not
|
||||||
// returned as errors.
|
// returned as errors.
|
||||||
@ -59,6 +72,13 @@ func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) {
|
|||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
tracer := trace.TracerFromContext(ctx)
|
tracer := trace.TracerFromContext(ctx)
|
||||||
|
|
||||||
|
// Capture Request Body
|
||||||
|
var reqBody []byte
|
||||||
|
if c.Request.Body != nil {
|
||||||
|
reqBody, _ = io.ReadAll(c.Request.Body)
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(reqBody)) // Restore body
|
||||||
|
}
|
||||||
|
|
||||||
spanName := c.FullPath()
|
spanName := c.FullPath()
|
||||||
method := c.Request.Method
|
method := c.Request.Method
|
||||||
|
|
||||||
@ -78,13 +98,39 @@ func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) {
|
|||||||
attribute.String("http.request_id", requestId),
|
attribute.String("http.request_id", requestId),
|
||||||
semconv.HTTPRouteKey.String(c.FullPath()),
|
semconv.HTTPRouteKey.String(c.FullPath()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Record Request Body (limit to 1MB)
|
||||||
|
if len(reqBody) > 0 {
|
||||||
|
limit := 1048576
|
||||||
|
if len(reqBody) > limit {
|
||||||
|
span.SetAttributes(attribute.String("http.request.body", string(reqBody[:limit])+"...(truncated)"))
|
||||||
|
} else {
|
||||||
|
span.SetAttributes(attribute.String("http.request.body", string(reqBody)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// context with request host
|
// context with request host
|
||||||
ctx = context.WithValue(ctx, constant.CtxKeyRequestHost, c.Request.Host)
|
ctx = context.WithValue(ctx, constant.CtxKeyRequestHost, c.Request.Host)
|
||||||
// restructure context
|
// restructure context
|
||||||
c.Request = c.Request.WithContext(ctx)
|
c.Request = c.Request.WithContext(ctx)
|
||||||
|
|
||||||
|
// Wrap ResponseWriter to capture Response Body
|
||||||
|
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
|
||||||
|
c.Writer = blw
|
||||||
|
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
||||||
|
// Record Response Body (limit to 1MB)
|
||||||
|
respBody := blw.body.String()
|
||||||
|
if len(respBody) > 0 {
|
||||||
|
limit := 1048576
|
||||||
|
if len(respBody) > limit {
|
||||||
|
span.SetAttributes(attribute.String("http.response.body", respBody[:limit]+"...(truncated)"))
|
||||||
|
} else {
|
||||||
|
span.SetAttributes(attribute.String("http.response.body", respBody))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handle response related attributes
|
// handle response related attributes
|
||||||
status := c.Writer.Status()
|
status := c.Writer.Status()
|
||||||
span.SetStatus(statusByWriter(status))
|
span.SetStatus(statusByWriter(status))
|
||||||
@ -97,7 +143,5 @@ func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) {
|
|||||||
span.RecordError(err.Err)
|
span.RecordError(err.Err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span.SetAttributes(semconv.HTTPResponseBodySizeKey.Int(c.Writer.Size()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -75,18 +77,36 @@ func (m *defaultUserModel) FindUsersSubscribeBySubscribeId(ctx context.Context,
|
|||||||
func (m *defaultUserModel) QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error) {
|
func (m *defaultUserModel) QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error) {
|
||||||
var list []*SubscribeDetails
|
var list []*SubscribeDetails
|
||||||
key := fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId)
|
key := fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId)
|
||||||
|
|
||||||
|
// 1. Get includeExpired from Context
|
||||||
|
includeExpired := ""
|
||||||
|
if v := ctx.Value(constant.CtxKeyIncludeExpired); v != nil {
|
||||||
|
includeExpired, _ = v.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If query mode is different, must modify Cache Key
|
||||||
|
if includeExpired == "all" {
|
||||||
|
key += ":all"
|
||||||
|
}
|
||||||
|
|
||||||
err := m.QueryCtx(ctx, &list, key, func(conn *gorm.DB, v interface{}) error {
|
err := m.QueryCtx(ctx, &list, key, func(conn *gorm.DB, v interface{}) error {
|
||||||
// 获取当前时间
|
// Base condition
|
||||||
now := time.Now()
|
db := conn.Model(&Subscribe{}).Where("`user_id` = ?", userId)
|
||||||
// 获取当前时间向前推 7 天
|
|
||||||
sevenDaysAgo := time.Now().Add(-7 * 24 * time.Hour)
|
|
||||||
// 基础条件查询
|
|
||||||
conn = conn.Model(&Subscribe{}).Where("`user_id` = ?", userId)
|
|
||||||
if len(status) > 0 {
|
if len(status) > 0 {
|
||||||
conn = conn.Where("`status` IN ?", status)
|
db = db.Where("`status` IN ?", status)
|
||||||
}
|
}
|
||||||
// 订阅过期时间大于当前时间或者订阅结束时间大于当前时间
|
|
||||||
return conn.Where("`expire_time` > ? OR `finished_at` >= ? OR `expire_time` = ?", now, sevenDaysAgo, time.UnixMilli(0)).
|
// 3. Adjust SQL based on param
|
||||||
|
if includeExpired == "all" {
|
||||||
|
// Mode A: Query all history
|
||||||
|
return db.Order("created_at DESC").Preload("Subscribe").Find(&list).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode B: Default only query valid subscriptions
|
||||||
|
// Logic: ExpireTime > Now OR FinishedAt >= 7 days ago OR ExpireTime = 0 (Never expire)
|
||||||
|
now := time.Now()
|
||||||
|
sevenDaysAgo := now.Add(-7 * 24 * time.Hour)
|
||||||
|
return db.Where("`expire_time` > ? OR `finished_at` >= ? OR `expire_time` = ?", now, sevenDaysAgo, time.UnixMilli(0)).
|
||||||
Preload("Subscribe").
|
Preload("Subscribe").
|
||||||
Find(&list).Error
|
Find(&list).Error
|
||||||
})
|
})
|
||||||
|
|||||||
@ -56,6 +56,10 @@ func parseDefaultValue(kind reflect.Kind, defaultValue string) any {
|
|||||||
var i uint32
|
var i uint32
|
||||||
_, _ = fmt.Sscanf(defaultValue, "%d", &i)
|
_, _ = fmt.Sscanf(defaultValue, "%d", &i)
|
||||||
return i
|
return i
|
||||||
|
case reflect.Float64:
|
||||||
|
var f float64
|
||||||
|
_, _ = fmt.Sscanf(defaultValue, "%f", &f)
|
||||||
|
return f
|
||||||
default:
|
default:
|
||||||
fmt.Printf("类型 %v 没有处理, 值为: %v \n", kind, defaultValue)
|
fmt.Printf("类型 %v 没有处理, 值为: %v \n", kind, defaultValue)
|
||||||
panic("unhandled default case")
|
panic("unhandled default case")
|
||||||
|
|||||||
@ -3,10 +3,11 @@ package constant
|
|||||||
type CtxKey string
|
type CtxKey string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CtxKeyUser CtxKey = "user"
|
CtxKeyUser CtxKey = "user"
|
||||||
CtxKeySessionID CtxKey = "sessionId"
|
CtxKeySessionID CtxKey = "sessionId"
|
||||||
CtxKeyRequestHost CtxKey = "requestHost"
|
CtxKeyRequestHost CtxKey = "requestHost"
|
||||||
CtxKeyPlatform CtxKey = "platform"
|
CtxKeyPlatform CtxKey = "platform"
|
||||||
CtxKeyPayment CtxKey = "payment"
|
CtxKeyPayment CtxKey = "payment"
|
||||||
LoginType CtxKey = "loginType"
|
LoginType CtxKey = "loginType"
|
||||||
|
CtxKeyIncludeExpired CtxKey = "includeExpired"
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user