feat: 完成操作日志功能 (#6870)

This commit is contained in:
ssongliu 2024-10-28 18:52:05 +08:00 committed by GitHub
parent 4bb0974c4a
commit 4b84124801
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 8700 additions and 4983 deletions

View File

@ -352,7 +352,7 @@ func (b *BaseApi) UpdatePHPConfig(c *gin.Context) {
// @Summary Update php conf file
// @Description 更新 php 配置文件
// @Accept json
// @Param request body request.WebsitePHPFileUpdate true "request"
// @Param request body request.PHPFileUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /runtimes/php/update [post]
@ -415,7 +415,7 @@ func (b *BaseApi) UpdateFPMConfig(c *gin.Context) {
// @Description 获取 fpm 配置
// @Accept json
// @Param id path integer true "request"
// @Success 200 {object} response.FPMConfig
// @Success 200 {object} request.FPMConfig
// @Security ApiKeyAuth
// @Router /runtimes/php/fpm/config/:id [get]
func (b *BaseApi) GetFPMConfig(c *gin.Context) {
@ -437,7 +437,7 @@ func (b *BaseApi) GetFPMConfig(c *gin.Context) {
// @Description 获取 supervisor 进程
// @Accept json
// @Param id path integer true "request"
// @Success 200 {object} response.SupervisorProcess
// @Success 200 {object} response.SupervisorProcessConfig
// @Security ApiKeyAuth
// @Router /runtimes/supervisor/process/:id [get]
func (b *BaseApi) GetSupervisorProcess(c *gin.Context) {

View File

@ -52,7 +52,7 @@ func (b *BaseApi) CreateSnapshot(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/snapshot/recrete [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"重试创建快照 [name]","formatEN":recrete the snapshot [name]"}
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"重试创建快照 [name]","formatEN":"recrete the snapshot [name]"}
func (b *BaseApi) RecreateSnapshot(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {

View File

@ -995,7 +995,7 @@ func (b *BaseApi) GetRealIPConfig(c *gin.Context) {
// @Description 获取网站资源
// @Accept json
// @Param id path int true "id"
// @Success 200 {object} response.WebsiteResource
// @Success 200 {object} response.Resource
// @Security ApiKeyAuth
// @Router /websites/resource/{id} [get]
func (b *BaseApi) GetWebsiteResource(c *gin.Context) {
@ -1016,7 +1016,7 @@ func (b *BaseApi) GetWebsiteResource(c *gin.Context) {
// @Summary Get databases
// @Description 获取数据库列表
// @Accept json
// @Success 200 {object} response.WebsiteDatabase
// @Success 200 {object} response.Database
// @Security ApiKeyAuth
// @Router /websites/databases [get]
func (b *BaseApi) GetWebsiteDatabase(c *gin.Context) {

View File

@ -100,6 +100,9 @@ func (u *CronjobService) handleShell(cronjob model.Cronjob, logPath string) erro
scriptFile, _ := os.ReadFile(cronjob.Script)
return cmd.ExecShell(logPath, 24*time.Hour, "docker", "exec", cronjob.ContainerName, command, "-c", strings.ReplaceAll(string(scriptFile), "\"", "\\\""))
}
if len(cronjob.Executor) == 0 {
cronjob.Executor = "bash"
}
if cronjob.ScriptMode == "input" {
fileItem := path.Join(global.CONF.System.BaseDir, "1panel", "task", "shell", cronjob.Name, cronjob.Name+".sh")
_ = os.MkdirAll(path.Dir(fileItem), os.ModePerm)
@ -111,8 +114,14 @@ func (u *CronjobService) handleShell(cronjob model.Cronjob, logPath string) erro
if _, err := shellFile.WriteString(cronjob.Script); err != nil {
return err
}
if len(cronjob.User) == 0 {
return cmd.ExecShell(logPath, 24*time.Hour, cronjob.Executor, fileItem)
}
return cmd.ExecShell(logPath, 24*time.Hour, "sudo", "-u", cronjob.User, cronjob.Executor, fileItem)
}
if len(cronjob.User) == 0 {
return cmd.ExecShell(logPath, 24*time.Hour, cronjob.Executor, cronjob.Script)
}
return cmd.ExecShell(logPath, 24*time.Hour, "sudo", "-u", cronjob.User, cronjob.Executor, cronjob.Script)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,76 @@ package docs
import (
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestGenerateXlog(t *testing.T) {
fset := token.NewFileSet()
apiDirs := []string{"../../../agent/app/api/v2", "../../../core/app/api/v2", "../../../agent/xpack/app/api/v2", "../../../core/xpack/app/api/v2"}
xlogMap := make(map[string]operationJson)
for _, dir := range apiDirs {
if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
fileItem, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return err
}
for _, decl := range fileItem.Decls {
switch d := decl.(type) {
case *ast.FuncDecl:
if d.Doc != nil {
routerContent := ""
logContent := ""
for _, comment := range d.Doc.List {
if strings.HasPrefix(comment.Text, "// @Router") {
routerContent = replaceStr(comment.Text, "// @Router", "[post]", "[get]", " ")
}
if strings.HasPrefix(comment.Text, "// @x-panel-log") {
logContent = replaceStr(comment.Text, "// @x-panel-log", " ")
}
}
if len(routerContent) != 0 && len(logContent) != 0 {
var item operationJson
if err := json.Unmarshal([]byte(logContent), &item); err != nil {
panic(fmt.Sprintf("json unamrshal failed, err: %v", err))
}
xlogMap[routerContent] = item
}
}
}
}
return nil
}); err != nil {
panic(err)
}
}
newJson, err := json.MarshalIndent(xlogMap, "", "\t")
if err != nil {
panic(fmt.Sprintf("json marshal for new file failed, err: %v", err))
}
if err := os.WriteFile("x-log.json", newJson, 0640); err != nil {
panic(fmt.Sprintf("write new swagger.json failed, err: %v", err))
}
}
func TestGenerateSwaggerDoc(t *testing.T) {
workDir := "/Users/slooop/Documents/mycode/1Panel"
swagBin := "/Users/slooop/.gvm/pkgsets/go1.22.4/global/bin/swag"
workDir := "/usr/songliu/1Panel"
swagBin := "/root/go/bin/swag"
cmd1 := exec.Command(swagBin, "init", "-o", workDir+"/cmd/server/docs/docs_agent", "-d", workDir+"/agent", "-g", "./cmd/server/main.go")
cmd1.Dir = workDir
@ -68,26 +129,6 @@ func TestGenerateSwaggerDoc(t *testing.T) {
newSwagger.Paths[key] = val
}
newXLog := make(map[string]interface{})
for key, val := range newSwagger.Paths {
methodMap, isMethodMap := val.(map[string]interface{})
if !isMethodMap {
continue
}
dataMap, hasPost := methodMap["post"]
if !hasPost {
continue
}
data, isDataMap := dataMap.(map[string]interface{})
if !isDataMap {
continue
}
xLog, hasXLog := data["x-panel-log"]
if !hasXLog {
continue
}
newXLog[key] = xLog
}
newJson, err := json.MarshalIndent(newSwagger, "", "\t")
if err != nil {
fmt.Printf("json marshal for new file failed, err: %v", err)
@ -103,16 +144,6 @@ func TestGenerateSwaggerDoc(t *testing.T) {
return
}
newXLogFile, err := json.MarshalIndent(newXLog, "", "\t")
if err != nil {
fmt.Printf("json marshal for new x-log file failed, err: %v", err)
return
}
if err := os.WriteFile("x-log.json", newXLogFile, 0640); err != nil {
fmt.Printf("write new x-log.json failed, err: %v", err)
return
}
_ = os.RemoveAll(workDir + "/cmd/server/docs/docs_agent")
_ = os.RemoveAll(workDir + "/cmd/server/docs/docs_core")
}
@ -150,3 +181,26 @@ func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}`
}
func replaceStr(val string, rep ...string) string {
for _, item := range rep {
val = strings.ReplaceAll(val, item, "")
}
return val
}
type operationJson struct {
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"`
}

File diff suppressed because it is too large Load Diff

View File

@ -138,7 +138,7 @@ func (b *BaseApi) SearchHost(c *gin.Context) {
// @Summary Delete host
// @Description 删除主机
// @Accept json
// @Param request body dto.BatchDeleteReq true "request"
// @Param request body dto.OperateByIDs true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /core/hosts/del [post]

View File

@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"path"
"strings"
"time"
@ -15,8 +16,9 @@ import (
"github.com/1Panel-dev/1Panel/core/app/service"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/utils/copier"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
func OperationLog() gin.HandlerFunc {
@ -36,43 +38,16 @@ func OperationLog() gin.HandlerFunc {
Path: pathItem,
UserAgent: c.Request.UserAgent(),
}
var (
swagger swaggerJson
operationDic operationJson
)
swagger := make(map[string]operationJson)
if err := json.Unmarshal(docs.XLogJson, &swagger); err != nil {
c.Next()
return
}
path, hasPath := swagger.Paths[record.Path]
operationDic, hasPath := swagger[record.Path]
if !hasPath {
c.Next()
return
}
methodMap, isMethodMap := path.(map[string]interface{})
if !isMethodMap {
c.Next()
return
}
dataMap, hasPost := methodMap["post"]
if !hasPost {
c.Next()
return
}
data, isDataMap := dataMap.(map[string]interface{})
if !isDataMap {
c.Next()
return
}
xlog, hasXlog := data["x-panel-log"]
if !hasXlog {
c.Next()
return
}
if err := copier.Copy(&operationDic, xlog); err != nil {
c.Next()
return
}
if len(operationDic.FormatZH) == 0 {
c.Next()
return
@ -93,21 +68,27 @@ func OperationLog() gin.HandlerFunc {
}
}
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)
_ = global.DB.Raw(sql, value).Scan(&names)
_ = dbItem.Raw(sql, value).Scan(&names)
} else {
_ = global.DB.Raw(fmt.Sprintf("select %s from %s where %s = ?;", funcs.OutputColumn, funcs.DB, funcs.InputColumn), value).Scan(&names)
_ = 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+"]") {
@ -168,10 +149,6 @@ func OperationLog() gin.HandlerFunc {
}
}
type swaggerJson struct {
Paths map[string]interface{} `json:"paths"`
}
type operationJson struct {
API string `json:"api"`
Method string `json:"method"`
@ -206,7 +183,7 @@ func (r responseBodyWriter) Write(b []byte) (int, error) {
}
func loadLogInfo(path string) string {
path = strings.ReplaceAll(path, "/api/v2", "")
path = replaceStr(path, "/api/v2", "/core", "/xpack")
if !strings.Contains(path, "/") {
return ""
}
@ -216,3 +193,42 @@ func loadLogInfo(path string) string {
}
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
}

View File

@ -18,7 +18,7 @@ export const getSystemLogs = (name: string) => {
};
export const cleanLogs = (param: Log.CleanLog) => {
return http.post(`/logs/clean`, param);
return http.post(`/core/logs/clean`, param);
};
export const searchTasks = (req: Log.SearchTaskReq) => {

View File

@ -6,19 +6,19 @@ import { Setting } from '../interface/setting';
// license
export const UploadFileData = (params: FormData) => {
return http.upload('/licenses/upload', params);
return http.upload('/xpack/licenses/upload', params);
};
export const getLicense = () => {
return http.get<Setting.License>(`/licenses/get`);
return http.get<Setting.License>(`/xpack/licenses/get`);
};
export const getLicenseStatus = () => {
return http.get<Setting.LicenseStatus>(`/licenses/get/status`);
return http.get<Setting.LicenseStatus>(`/xpack/licenses/get/status`);
};
export const syncLicense = () => {
return http.post(`/licenses/sync`);
return http.post(`/xpack/licenses/sync`);
};
export const unbindLicense = () => {
return http.post(`/licenses/unbind`);
return http.post(`/xpack/licenses/unbind`);
};
// agent

View File

@ -1180,6 +1180,8 @@ const message = {
runtimes: 'Runtime',
process: 'Process',
toolbox: 'Toolbox',
tampers: 'Tamper',
xsetting: 'Interface Settings',
logs: 'Panel Logs',
settings: 'Panel Setting',
cronjobs: 'Cronjob',

View File

@ -1118,6 +1118,8 @@ const message = {
runtimes: '運行環境',
process: '進程管理',
toolbox: '工具箱',
tampers: '防篡改',
xsetting: '界面設定',
logs: '日誌審計',
settings: '面板設置',
cronjobs: '計劃任務',

View File

@ -1120,6 +1120,8 @@ const message = {
runtimes: '运行环境',
process: '进程管理',
toolbox: '工具箱',
tampers: '防篡改',
xsetting: '界面设置',
logs: '日志审计',
settings: '面板设置',
cronjobs: '计划任务',

View File

@ -237,11 +237,7 @@
<el-card>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item
:label="$t('commons.table.user')"
prop="user"
:rules="Rules.requiredSelect"
>
<el-form-item :label="$t('commons.table.user')" prop="user">
<el-select filterable v-model="dialogData.rowData!.user">
<div v-for="item in userOptions" :key="item">
<el-option :value="item" :label="item" />
@ -250,11 +246,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
:label="$t('cronjob.executor')"
prop="executor"
:rules="Rules.requiredSelect"
>
<el-form-item :label="$t('cronjob.executor')" prop="executor">
<el-checkbox border v-model="dialogData.rowData!.isExecutorCustom">
{{ $t('container.custom') }}
</el-checkbox>

View File

@ -150,9 +150,15 @@ const loadDetail = (log: string) => {
if (log.indexOf('[enable]') !== -1) {
log = log.replace('[enable]', '[' + i18n.global.t('commons.button.enable') + ']');
}
if (log.indexOf('[Enable]') !== -1) {
log = log.replace('[Enable]', '[' + i18n.global.t('commons.button.enable') + ']');
}
if (log.indexOf('[disable]') !== -1) {
log = log.replace('[disable]', '[' + i18n.global.t('commons.button.disable') + ']');
}
if (log.indexOf('[Disable]') !== -1) {
log = log.replace('[Disable]', '[' + i18n.global.t('commons.button.disable') + ']');
}
if (log.indexOf('[light]') !== -1) {
log = log.replace('[light]', '[' + i18n.global.t('setting.light') + ']');
}