You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1Panel/core/middleware/operation.go

235 lines
6.4 KiB

package middleware
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"net/http"
"path"
"strings"
"time"
"github.com/1Panel-dev/1Panel/cmd/server/docs"
"github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/app/service"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
func OperationLog() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.Contains(c.Request.URL.Path, "search") || c.Request.Method == http.MethodGet {
c.Next()
return
}
source := loadLogInfo(c.Request.URL.Path)
pathItem := strings.TrimPrefix(c.Request.URL.Path, "/api/v2")
pathItem = strings.TrimPrefix(pathItem, "/api/v2/core")
record := &model.OperationLog{
Source: source,
IP: c.ClientIP(),
Method: strings.ToLower(c.Request.Method),
Path: pathItem,
UserAgent: c.Request.UserAgent(),
}
swagger := make(map[string]operationJson)
if err := json.Unmarshal(docs.XLogJson, &swagger); err != nil {
c.Next()
return
}
operationDic, hasPath := swagger[record.Path]
if !hasPath {
c.Next()
return
}
if len(operationDic.FormatZH) == 0 {
c.Next()
return
}
formatMap := make(map[string]interface{})
if len(operationDic.BodyKeys) != 0 {
body, err := io.ReadAll(c.Request.Body)
if err == nil {
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
}
bodyMap := make(map[string]interface{})
_ = json.Unmarshal(body, &bodyMap)
for _, key := range operationDic.BodyKeys {
if _, ok := bodyMap[key]; ok {
formatMap[key] = bodyMap[key]
}
}
}
if len(operationDic.BeforeFunctions) != 0 {
dbItem, err := newDB(record.Path)
if err != nil {
c.Next()
return
}
for _, funcs := range operationDic.BeforeFunctions {
for key, value := range formatMap {
if funcs.InputValue == key {
var names []string
if funcs.IsList {
sql := fmt.Sprintf("SELECT %s FROM %s where %s in (?);", funcs.OutputColumn, funcs.DB, funcs.InputColumn)
_ = dbItem.Raw(sql, value).Scan(&names)
} else {
_ = dbItem.Raw(fmt.Sprintf("select %s from %s where %s = ?;", funcs.OutputColumn, funcs.DB, funcs.InputColumn), value).Scan(&names)
}
formatMap[funcs.OutputValue] = strings.Join(names, ",")
break
}
}
}
closeDB(dbItem)
}
for key, value := range formatMap {
if strings.Contains(operationDic.FormatEN, "["+key+"]") {
if arrays, ok := value.([]string); ok {
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", fmt.Sprintf("[%v]", strings.Join(arrays, ",")))
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", fmt.Sprintf("[%v]", strings.Join(arrays, ",")))
} else {
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", fmt.Sprintf("[%v]", value))
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", fmt.Sprintf("[%v]", value))
}
}
}
record.DetailEN = strings.ReplaceAll(operationDic.FormatEN, "[]", "")
record.DetailZH = strings.ReplaceAll(operationDic.FormatZH, "[]", "")
writer := responseBodyWriter{
ResponseWriter: c.Writer,
body: &bytes.Buffer{},
}
c.Writer = writer
now := time.Now()
c.Next()
datas := writer.body.Bytes()
if c.Request.Header.Get("Content-Encoding") == "gzip" {
buf := bytes.NewReader(writer.body.Bytes())
reader, err := gzip.NewReader(buf)
if err != nil {
record.Status = constant.StatusFailed
record.Message = fmt.Sprintf("gzip new reader failed, err: %v", err)
latency := time.Since(now)
record.Latency = latency
if err := service.NewILogService().CreateOperationLog(record); err != nil {
global.LOG.Errorf("create operation record failed, err: %v", err)
}
return
}
defer reader.Close()
datas, _ = io.ReadAll(reader)
}
var res response
_ = json.Unmarshal(datas, &res)
if res.Code == 200 {
record.Status = constant.StatusSuccess
} else {
record.Status = constant.StatusFailed
record.Message = res.Message
}
latency := time.Since(now)
record.Latency = latency
if err := service.NewILogService().CreateOperationLog(record); err != nil {
global.LOG.Errorf("create operation record failed, err: %v", err)
}
}
}
type operationJson struct {
API string `json:"api"`
Method string `json:"method"`
BodyKeys []string `json:"bodyKeys"`
ParamKeys []string `json:"paramKeys"`
BeforeFunctions []functionInfo `json:"beforeFunctions"`
FormatZH string `json:"formatZH"`
FormatEN string `json:"formatEN"`
}
type functionInfo struct {
InputColumn string `json:"input_column"`
InputValue string `json:"input_value"`
IsList bool `json:"isList"`
DB string `json:"db"`
OutputColumn string `json:"output_column"`
OutputValue string `json:"output_value"`
}
type response struct {
Code int `json:"code"`
Message string `json:"message"`
}
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (r responseBodyWriter) Write(b []byte) (int, error) {
r.body.Write(b)
return r.ResponseWriter.Write(b)
}
func loadLogInfo(path string) string {
path = replaceStr(path, "/api/v2", "/core", "/xpack")
if !strings.Contains(path, "/") {
return ""
}
pathArrays := strings.Split(path, "/")
if len(pathArrays) < 2 {
return ""
}
return pathArrays[1]
}
func newDB(pathItem string) (*gorm.DB, error) {
dbFile := ""
switch {
case strings.HasPrefix(pathItem, "/core"):
dbFile = path.Join(global.CONF.System.BaseDir, "1panel/db/core.db")
case strings.HasPrefix(pathItem, "/xpack"):
dbFile = path.Join(global.CONF.System.BaseDir, "1panel/db/xpack/xpack.db")
default:
dbFile = path.Join(global.CONF.System.BaseDir, "1panel/db/agent.db")
}
db, _ := gorm.Open(sqlite.Open(dbFile), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetConnMaxIdleTime(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
return db, nil
}
func closeDB(db *gorm.DB) {
sqlDB, err := db.DB()
if err != nil {
return
}
_ = sqlDB.Close()
}
func replaceStr(val string, rep ...string) string {
for _, item := range rep {
val = strings.ReplaceAll(val, item, "")
}
return val
}