8. 插件编辑安全和权限验证机制
8.1 插件编辑身份认证
JWT Token验证:
- 编辑插件的所有API请求都需要携带有效的JWT Token
- Token包含用户ID、工作空间权限等关键信息
- 通过中间件统一验证Token的有效性和完整性
go
// 插件编辑身份验证中间件
func PluginEditAuthMiddleware() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
token := ctx.GetHeader("Authorization")
if token == nil {
ctx.JSON(401, gin.H{"error": "编辑插件需要登录认证"})
ctx.Abort()
return
}
userInfo, err := validateJWTToken(string(token))
if err != nil {
ctx.JSON(401, gin.H{"error": "Token无效,无法编辑插件"})
ctx.Abort()
return
}
// 验证用户是否有编辑插件的权限
if !userInfo.HasPluginEditPermission {
ctx.JSON(403, gin.H{"error": "用户无编辑插件权限"})
ctx.Abort()
return
}
ctx.Set("user_id", userInfo.UserID)
ctx.Set("space_id", userInfo.SpaceID)
ctx.Set("editor_id", userInfo.UserID)
ctx.Next()
}
}
8.2 插件编辑工作空间权限控制
空间隔离机制:
- 每个用户只能编辑其所属工作空间中的插件
- 通过
space_id
字段实现插件编辑权限隔离 - 在插件编辑操作中强制验证空间权限
go
// 插件编辑工作空间权限验证
func (s *PluginApplicationService) validatePluginEditSpacePermission(ctx context.Context, req *service.UpdateDraftPluginRequest) error {
userSpaceID := ctx.Value("space_id").(int64)
// 获取插件信息以验证所属空间
plugin, err := s.DomainSVC.GetDraftPlugin(ctx, req.PluginID)
if err != nil {
return fmt.Errorf("获取插件信息失败: %w", err)
}
if plugin == nil {
return errors.New("插件不存在")
}
// 验证插件所属空间是否与用户所属空间一致
if plugin.SpaceID != userSpaceID {
return errors.New("无权限编辑该工作空间的插件")
}
// 检查工作空间是否允许编辑插件
spaceConfig, err := s.spaceService.GetSpaceConfig(ctx, userSpaceID)
if err != nil {
return fmt.Errorf("获取工作空间配置失败: %w", err)
}
if !spaceConfig.AllowPluginEdit {
return errors.New("该工作空间不允许编辑插件")
}
return nil
}
8.3 插件编辑资源级权限验证
插件编辑权限验证:
- 严格验证用户是否具有插件编辑权限
- 支持插件所有者、工作空间管理员和具有编辑权限的用户进行编辑
- 实现插件级别的权限隔离和编辑锁定机制
go
// 插件编辑权限验证
func (s *PluginApplicationService) validatePluginEditPermission(ctx context.Context, req *service.UpdateDraftPluginRequest) error {
userID := ctx.Value("user_id").(int64)
// 获取插件信息
plugin, err := s.DomainSVC.GetDraftPlugin(ctx, req.PluginID)
if err != nil {
return fmt.Errorf("获取插件信息失败: %w", err)
}
if plugin == nil {
return errors.New("插件不存在")
}
// 检查用户是否为插件所有者
if plugin.DeveloperID == userID {
return nil // 插件所有者可以编辑
}
// 检查用户是否为工作空间管理员
isAdmin, err := s.userService.IsWorkspaceAdmin(ctx, userID, plugin.SpaceID)
if err != nil {
return fmt.Errorf("验证工作空间管理员权限失败: %w", err)
}
if isAdmin {
return nil // 工作空间管理员可以编辑
}
// 检查用户是否具有特定插件的编辑权限
hasEditPermission, err := s.permissionService.HasPluginEditPermission(ctx, userID, req.PluginID)
if err != nil {
return fmt.Errorf("验证插件编辑权限失败: %w", err)
}
if !hasEditPermission {
return errorx.New(errno.ErrPluginPermissionCode,
errorx.KV(errno.PluginMsgKey, "用户无权限编辑该插件"),
errorx.KV("user_id", userID),
errorx.KV("plugin_id", req.PluginID))
}
// 检查编辑频率限制
editCount, err := s.getUserPluginEditCount(ctx, userID, time.Now().Add(-time.Hour))
if err != nil {
return fmt.Errorf("检查插件编辑频率失败: %w", err)
}
if editCount >= 30 { // 每小时最多编辑30次
return errorx.New(errno.ErrPluginEditRateLimitCode,
errorx.KV("user_id", userID),
errorx.KV("edit_count", editCount))
}
// 检查插件锁定状态
lockInfo, err := s.lockService.GetPluginLock(ctx, req.PluginID)
if err != nil {
return fmt.Errorf("检查插件锁定状态失败: %w", err)
}
if lockInfo != nil && lockInfo.UserID != userID {
// 如果插件被其他用户锁定,检查锁是否过期
if time.Now().Before(lockInfo.ExpireTime) {
return errorx.New(errno.ErrPluginLockedCode,
errorx.KV("plugin_id", req.PluginID),
errorx.KV("locked_by", lockInfo.UserID),
errorx.KV("expire_at", lockInfo.ExpireTime))
}
// 如果锁已过期,自动释放
s.lockService.ReleasePluginLock(ctx, req.PluginID, lockInfo.LockID)
}
return nil
}
8.4 插件编辑API访问控制
编辑请求频率限制:
- 实现基于用户的插件编辑频率限制
- 防止恶意批量编辑插件
- 支持不同用户等级的差异化编辑限流策略
编辑操作安全验证:
- 严格验证编辑请求的合法性
- 实现编辑冲突检测和锁定机制
- 使用多重安全检查机制
- 级联更新安全验证,确保关联工具数据的完整性
go
// 插件编辑参数验证
func validatePluginEditRequest(req *service.UpdateDraftPluginRequest) error {
if req.PluginID <= 0 {
return errors.New("无效的插件ID")
}
// 验证插件名称(如果更新)
if req.Name != nil {
if *req.Name == "" {
return errors.New("插件名称不能为空")
}
if len(*req.Name) > 100 {
return errors.New("插件名称长度不能超过100字符")
}
}
// 验证插件描述(如果更新)
if req.Desc != nil {
if *req.Desc == "" {
return errors.New("插件描述不能为空")
}
if len(*req.Desc) > 500 {
return errors.New("插件描述长度不能超过500字符")
}
}
// 验证服务器URL(如果更新)
if req.ServerURL != nil {
if *req.ServerURL == "" {
return errors.New("服务器URL不能为空")
}
if !isValidURL(*req.ServerURL) {
return errors.New("无效的服务器URL格式")
}
}
// 验证编辑版本号
if req.EditVersion <= 0 {
return errors.New("无效的编辑版本号")
}
return nil
}
// 插件编辑操作安全检查
func (s *PluginApplicationService) validatePluginEditSafety(ctx context.Context, req *service.UpdateDraftPluginRequest) error {
userID := ctx.Value("user_id").(int64)
// 获取插件信息进行版本验证
plugin, err := s.DomainSVC.GetDraftPlugin(ctx, req.PluginID)
if err != nil {
return fmt.Errorf("获取插件信息失败: %w", err)
}
if plugin == nil {
return errors.New("插件不存在")
}
// 版本冲突检测
if req.EditVersion != plugin.EditVersion {
return errorx.New(errno.ErrPluginVersionConflictCode,
errorx.KV("plugin_id", req.PluginID),
errorx.KV("client_version", req.EditVersion),
errorx.KV("server_version", plugin.EditVersion))
}
// 检查编辑频率限制
editCount, err := s.getUserPluginEditCount(ctx, userID, time.Now().Add(-time.Hour))
if err != nil {
return fmt.Errorf("检查插件编辑频率失败: %w", err)
}
if editCount >= 30 { // 每小时最多编辑30次
return errorx.New(errno.ErrPluginEditRateLimitCode,
errorx.KV("user_id", userID),
errorx.KV("edit_count", editCount))
}
// 如果更新了服务器URL,检查可访问性
if req.ServerURL != nil && *req.ServerURL != plugin.ServerURL {
accessible, err := s.checkServerURLAccessible(ctx, *req.ServerURL)
if err != nil {
return fmt.Errorf("检查服务器URL可访问性失败: %w", err)
}
if !accessible {
return errors.New("服务器URL不可访问")
}
}
// 检查URL安全性(如果更新)
if req.ServerURL != nil {
if err := s.validateURLSafety(ctx, *req.ServerURL); err != nil {
return fmt.Errorf("URL安全性验证失败: %w", err)
}
}
// 检查工作空间存储配额
if err := s.checkWorkspaceStorageQuota(ctx, plugin.SpaceID); err != nil {
return fmt.Errorf("工作空间存储配额检查失败: %w", err)
}
return nil
}
// 检查URL安全性
func (s *PluginApplicationService) validateURLSafety(ctx context.Context, url string) error {
// 检查URL是否在黑名单中
if s.urlBlacklist.Contains(url) {
return errors.New("URL在黑名单中,不允许使用")
}
// 检查URL是否包含敏感路径
if containsSensitivePath(url) {
return errors.New("URL包含敏感路径")
}
// 检查URL域名是否可信
if !s.isTrustedDomain(url) {
// 对不可信域名进行额外验证
if err := s.performAdditionalURLChecks(ctx, url); err != nil {
return err
}
}
return nil
}
// 编辑锁定机制
func (s *PluginApplicationService) acquirePluginEditLock(ctx context.Context, pluginID int64, userID int64) (string, error) {
// 尝试获取编辑锁,设置过期时间为5分钟
lockID, err := s.lockService.AcquirePluginLock(ctx, pluginID, userID, 5*time.Minute)
if err != nil {
return "", fmt.Errorf("获取编辑锁失败: %w", err)
}
if lockID == "" {
// 获取锁失败,检查是谁锁定的
lockInfo, _ := s.lockService.GetPluginLock(ctx, pluginID)
if lockInfo != nil {
return "", errorx.New(errno.ErrPluginLockedCode,
errorx.KV("plugin_id", pluginID),
errorx.KV("locked_by", lockInfo.UserID),
errorx.KV("expire_at", lockInfo.ExpireTime))
}
}
return lockID, nil
}
func (s *PluginApplicationService) getUserStorageUsage(ctx context.Context, userID int64) (int64, error) {
// 查询用户所有插件的存储使用量
plugins, err := s.DomainSVC.ListUserPlugins(ctx, userID)
if err != nil {
return 0, fmt.Errorf("获取用户插件列表失败: %w", err)
}
var totalSize int64
for _, plugin := range plugins {
// 计算插件manifest和openapi_doc的存储大小
if plugin.Manifest != nil {
totalSize += int64(len(plugin.Manifest))
}
if plugin.OpenapiDoc != nil {
totalSize += int64(len(plugin.OpenapiDoc))
}
}
return totalSize, nil
}
// 获取用户最大存储配额
func (s *PluginApplicationService) getMaxStorageQuota(userID int64) int64 {
// 根据用户等级返回不同的存储配额
// 这里简化处理,实际应该从用户配置中获取
return 100 * 1024 * 1024 // 100MB
}
// URL格式验证
func isValidURL(urlStr string) bool {
u, err := url.Parse(urlStr)
return err == nil && u.Scheme != "" && u.Host != ""
}
// 插件类型验证
func isValidPluginType(pluginType common.PluginType) bool {
validTypes := []common.PluginType{
common.PluginTypeHTTP,
common.PluginTypeLocal,
}
for _, validType := range validTypes {
if pluginType == validType {
return true
}
}
return false
}
9. 插件编辑错误处理和日志记录
9.1 插件编辑分层错误处理机制
插件编辑错误分类体系:
go
// 插件编辑错误类型定义
type PluginEditErrorType int
const (
// 插件编辑业务错误
ErrPluginEditBusiness PluginEditErrorType = iota + 1000
ErrPluginNameExists
ErrPluginPermissionDenied
ErrPluginEditRateLimit
ErrPluginInvalidParameters
ErrPluginServerURLNotAccessible
ErrPluginStorageQuotaExceeded
ErrPluginToolCascadeEditFailed
ErrPluginManifestInvalid
ErrPluginOpenapiDocInvalid
ErrPluginInvalidAuthType
ErrPluginInvalidIconURI
ErrPluginInvalidSpaceID
ErrPluginDuplicateName
ErrPluginEditConflict
ErrPluginLockRequired
ErrPluginLockExpired
ErrPluginLockNotFound
ErrPluginVersionConflict
ErrPluginEditOperationDenied
ErrPluginEditFieldInvalid
ErrPluginEditContentTooLong
// 插件编辑系统错误
ErrPluginEditSystem PluginEditErrorType = iota + 2000
ErrPluginDatabaseConnection
ErrPluginElasticSearchTimeout
ErrPluginServiceUnavailable
ErrPluginEditEventPublishFailed
ErrPluginIndexUpdateFailed
ErrPluginTransactionRollbackFailed
ErrPluginLockAcquireFailed
ErrPluginLockReleaseFailed
ErrPluginLockUpdateFailed
ErrPluginLockRenewFailed
ErrPluginVersionUpdateFailed
// 插件编辑网络错误
ErrPluginEditNetwork PluginEditErrorType = iota + 3000
ErrPluginEditRequestTimeout
ErrPluginEditConnectionRefused
ErrPluginEditServiceDown
ErrPluginEditESConnectionFailed
ErrPluginServerURLTimeout
ErrPluginValidationTimeout
ErrPluginLockServiceTimeout
)
插件编辑错误处理流程:
- 捕获阶段:在插件编辑各层级捕获具体错误
- 包装阶段:添加插件编辑操作相关上下文信息和错误码
- 记录阶段:根据错误级别记录插件编辑操作日志
- 响应阶段:返回用户友好的插件编辑错误信息
- 回滚阶段:插件编辑失败时进行必要的数据回滚操作
- 锁定处理:处理编辑锁定相关错误,包括锁定过期、锁定冲突
- 版本处理:处理版本冲突错误,提供版本比较和合并建议
- 重试机制:对于可重试的编辑错误提供重试建议
- 用户指导:为常见编辑错误提供解决方案指导
9.2 插件编辑统一错误响应格式
go
// 插件编辑错误响应结构
type PluginEditErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
TraceID string `json:"trace_id"`
PluginID int64 `json:"plugin_id,omitempty"`
Operation string `json:"operation"`
CanRetry bool `json:"can_retry"`
LockExpired bool `json:"lock_expired,omitempty"`
VersionMismatch bool `json:"version_mismatch,omitempty"`
CurrentVersion int64 `json:"current_version,omitempty"`
ToolsEdited int `json:"tools_edited,omitempty"`
ToolsFailed int `json:"tools_failed,omitempty"`
ValidationErrors []string `json:"validation_errors,omitempty"`
SuggestedFix string `json:"suggested_fix,omitempty"`
FieldErrors map[string]string `json:"field_errors,omitempty"`
LockInfo *LockInfo `json:"lock_info,omitempty"`
}
// 锁定信息结构
type LockInfo struct {
LockOwnerID int64 `json:"lock_owner_id,omitempty"`
LockOwnerName string `json:"lock_owner_name,omitempty"`
LockExpireTime int64 `json:"lock_expire_time,omitempty"`
LockAcquireTime int64 `json:"lock_acquire_time,omitempty"`
}
// 插件编辑错误处理中间件
func PluginEditErrorHandlerMiddleware() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
defer func() {
if err := recover(); err != nil {
traceID := ctx.GetString("trace_id")
userID := ctx.GetInt64("user_id")
spaceID := ctx.GetInt64("space_id")
pluginID := ctx.GetInt64("plugin_id")
logs.CtxErrorf(c, "Plugin edit panic recovered: %v, userID=%d, spaceID=%d, pluginID=%d, traceID=%s",
err, userID, spaceID, pluginID, traceID)
ctx.JSON(500, PluginEditErrorResponse{
Code: 5000,
Message: "插件编辑服务器内部错误",
TraceID: traceID,
Operation: "edit_plugin",
CanRetry: true,
SuggestedFix: "请稍后重试,如果问题持续存在请联系技术支持",
})
}
}()
ctx.Next()
}
}
// 插件编辑业务错误处理
func handlePluginEditBusinessError(ctx *app.RequestContext, err error) {
traceID := ctx.GetString("trace_id")
pluginID := ctx.GetInt64("plugin_id")
var response PluginEditErrorResponse
response.TraceID = traceID
response.PluginID = pluginID
response.Operation = "edit_plugin"
switch {
case errors.Is(err, errno.ErrPluginInvalidParamCode):
response.Code = 400
response.Message = "插件参数无效"
response.CanRetry = false
response.SuggestedFix = "请检查插件名称、描述、服务器URL等参数是否正确"
case errors.Is(err, errno.ErrPluginPermissionCode):
response.Code = 403
response.Message = "无权限编辑插件"
response.CanRetry = false
response.SuggestedFix = "请确保已登录且具有插件编辑权限"
case errors.Is(err, errno.ErrPluginInvalidManifest):
response.Code = 400
response.Message = "插件清单格式无效"
response.CanRetry = false
response.SuggestedFix = "请检查插件清单文件格式是否符合规范"
case errors.Is(err, errno.ErrPluginInvalidOpenapi3Doc):
response.Code = 400
response.Message = "OpenAPI文档格式无效"
response.CanRetry = false
response.SuggestedFix = "请检查OpenAPI文档格式是否符合OpenAPI 3.0规范"
case errors.Is(err, errno.ErrPluginEditConflict):
response.Code = 409
response.Message = "插件编辑冲突"
response.CanRetry = false
response.SuggestedFix = "该插件已被其他用户修改,请刷新页面后重试"
case errors.Is(err, errno.ErrPluginLockRequired):
response.Code = 400
response.Message = "插件锁定失败"
response.CanRetry = true
response.SuggestedFix = "请重新点击编辑按钮获取编辑权限"
case errors.Is(err, errno.ErrPluginLockExpired):
response.Code = 400
response.Message = "编辑锁定已过期"
response.CanRetry = true
response.LockExpired = true
response.SuggestedFix = "编辑锁定已过期,请重新获取编辑权限"
case errors.Is(err, errno.ErrPluginVersionConflict):
response.Code = 409
response.Message = "版本冲突"
response.CanRetry = false
response.VersionMismatch = true
response.SuggestedFix = "插件已被其他用户更新,请刷新后重新编辑"
case errors.Is(err, errno.ErrPluginEditRateLimit):
response.Code = 429
response.Message = "编辑操作过于频繁,请稍后再试"
response.CanRetry = true
response.SuggestedFix = "请等待一段时间后重试"
default:
response.Code = 500
response.Message = "插件编辑失败"
response.CanRetry = true
response.SuggestedFix = "请稍后重试,如果问题持续存在请联系技术支持"
}
ctx.JSON(response.Code, response)
}
// 插件编辑锁定错误处理
func handlePluginEditLockError(ctx *app.RequestContext, err error, lockInfo *LockInfo) {
traceID := ctx.GetString("trace_id")
pluginID := ctx.GetInt64("plugin_id")
var response PluginEditErrorResponse
response.TraceID = traceID
response.PluginID = pluginID
response.Operation = "edit_plugin"
response.LockInfo = lockInfo
switch {
case errors.Is(err, errno.ErrPluginLockAcquireFailed):
response.Code = 409
response.Message = "插件已被锁定"
response.CanRetry = false
response.SuggestedFix = "该插件正在被其他用户编辑,请稍后再试或联系管理员"
case errors.Is(err, errno.ErrPluginLockExpired):
response.Code = 400
response.Message = "编辑锁定已过期"
response.CanRetry = true
response.LockExpired = true
response.SuggestedFix = "编辑锁定已过期,请重新获取编辑权限"
case errors.Is(err, errno.ErrPluginLockReleaseFailed):
response.Code = 500
response.Message = "锁定释放失败"
response.CanRetry = true
response.SuggestedFix = "请刷新页面,如果编辑按钮仍然不可用,请联系管理员"
case errors.Is(err, errno.ErrPluginLockRenewFailed):
response.Code = 400
response.Message = "锁定续期失败"
response.CanRetry = true
response.SuggestedFix = "请刷新页面后重新编辑"
default:
response.Code = 500
response.Message = "锁定服务错误"
response.CanRetry = true
response.SuggestedFix = "锁定服务暂时不可用,请稍后重试"
}
ctx.JSON(response.Code, response)
}
// 插件编辑系统错误处理
func handlePluginEditSystemError(ctx *app.RequestContext, err error) {
traceID := ctx.GetString("trace_id")
pluginID := ctx.GetInt64("plugin_id")
var response PluginEditErrorResponse
response.TraceID = traceID
response.PluginID = pluginID
response.Operation = "edit_plugin"
switch {
case errors.Is(err, errno.ErrPluginDatabaseConnection):
response.Code = 500
response.Message = "插件数据库连接失败"
response.CanRetry = true
response.SuggestedFix = "数据库连接异常,请稍后重试"
case errors.Is(err, errno.ErrPluginElasticSearchTimeout):
response.Code = 500
response.Message = "插件索引操作超时"
response.CanRetry = true
response.SuggestedFix = "搜索服务响应超时,请稍后重试"
case errors.Is(err, errno.ErrPluginServiceUnavailable):
response.Code = 503
response.Message = "插件编辑服务暂时不可用"
response.CanRetry = true
response.SuggestedFix = "服务正在维护中,请稍后重试"
case errors.Is(err, errno.ErrPluginEditEventPublishFailed):
response.Code = 500
response.Message = "插件编辑事件发布失败"
response.CanRetry = true
response.SuggestedFix = "事件发布异常,插件已编辑但可能影响搜索,请稍后重试"
case errors.Is(err, errno.ErrPluginIndexUpdateFailed):
response.Code = 500
response.Message = "插件索引更新失败"
response.CanRetry = true
response.SuggestedFix = "搜索索引更新失败,插件已编辑但可能无法搜索到"
case errors.Is(err, errno.ErrPluginTransactionRollbackFailed):
response.Code = 500
response.Message = "插件编辑事务回滚失败"
response.CanRetry = false
response.SuggestedFix = "数据一致性异常,请联系技术支持"
case errors.Is(err, errno.ErrPluginLockServiceTimeout):
response.Code = 500
response.Message = "锁定服务超时"
response.CanRetry = true
response.SuggestedFix = "锁定服务暂时不可用,请稍后重试"
default:
response.Code = 5000
response.Message = "插件编辑失败"
response.Details = "服务器内部错误,请稍后重试"
response.CanRetry = true
response.SuggestedFix = "系统内部错误,请稍后重试或联系技术支持"
}
ctx.JSON(response.Code, response)
}
9.3 插件编辑日志记录策略
插件编辑日志级别定义:
- DEBUG:插件编辑详细调试信息,包括参数值、中间结果、数据转换过程
- INFO:插件编辑关键业务流程信息,如编辑开始、锁定获取、参数验证、数据更新、事件发布
- WARN:插件编辑潜在问题警告,如参数格式警告、性能警告、锁定即将过期
- ERROR:插件编辑错误信息,包括编辑失败、权限错误、锁定冲突、版本冲突
- FATAL:插件编辑严重错误,可能导致数据不一致或服务不可用
插件编辑结构化日志格式:
go
// 插件编辑日志记录示例
func (s *PluginApplicationService) UpdatePluginMeta(ctx context.Context, req *pluginAPI.UpdatePluginMetaRequest) (*pluginAPI.UpdatePluginMetaResponse, error) {
traceID := generateTraceID()
ctx = context.WithValue(ctx, "trace_id", traceID)
ctx = context.WithValue(ctx, "plugin_id", req.GetPluginID())
userID := ctxutil.GetUIDFromCtx(ctx)
// 记录插件编辑开始
logs.CtxInfof(ctx, "UpdatePluginMeta started, userID=%d, pluginID=%d, pluginName=%s, spaceID=%d, traceID=%s",
userID, req.GetPluginID(), req.GetName(), req.GetSpaceID(), traceID)
startTime := time.Now()
defer func() {
duration := time.Since(startTime)
logs.CtxInfof(ctx, "UpdatePluginMeta completed, duration=%dms, traceID=%s",
duration.Milliseconds(), traceID)
}()
// 记录锁定获取
logs.CtxInfof(ctx, "Acquiring plugin edit lock, pluginID=%d, userID=%d, traceID=%s",
req.GetPluginID(), userID, traceID)
// 记录版本验证
logs.CtxInfof(ctx, "Validating plugin version, pluginID=%d, currentVersion=%d, requestVersion=%d, traceID=%s",
req.GetPluginID(), currentVersion, req.GetVersion(), traceID)
// 记录关键步骤
logs.CtxInfof(ctx, "Validating plugin edit parameters, pluginID=%d, authType=%d, traceID=%s",
req.GetPluginID(), req.GetAuthType(), traceID)
// 权限验证日志
logs.CtxInfof(ctx, "Validating plugin edit permission, userID=%d, pluginID=%d, spaceID=%d, traceID=%s",
userID, req.GetPluginID(), req.GetSpaceID(), traceID)
// 数据库更新操作日志
logs.CtxInfof(ctx, "Updating plugin in database, pluginID=%d, traceID=%s",
req.GetPluginID(), traceID)
// 事件发布日志
logs.CtxInfof(ctx, "Publishing plugin edit event, pluginID=%d, traceID=%s",
req.GetPluginID(), traceID)
return resp, nil
}
// 插件编辑锁定日志记录
func (s *PluginApplicationService) logPluginEditLockEvent(ctx context.Context, operation string, pluginID int64, userID int64, lockExpireTime time.Time, err error) {
traceID := ctx.Value("trace_id").(string)
logData := map[string]interface{}{
"operation": operation,
"plugin_id": pluginID,
"user_id": userID,
"lock_expire_at": lockExpireTime.Unix(),
"trace_id": traceID,
"timestamp": time.Now().Unix(),
"success": err == nil,
}
if err != nil {
logData["error"] = err.Error()
logs.CtxErrorf(ctx, "Plugin edit lock %s failed: %+v", operation, logData)
} else {
logs.CtxInfof(ctx, "Plugin edit lock %s succeeded: %+v", operation, logData)
}
}
// 插件编辑操作审计日志
func (s *PluginApplicationService) logPluginEditAudit(ctx context.Context, operation string, pluginID int64, before map[string]interface{}, after map[string]interface{}) {
userID := ctx.Value("user_id").(int64)
spaceID := ctx.Value("space_id").(int64)
traceID := ctx.Value("trace_id").(string)
auditLog := map[string]interface{}{
"operation": operation,
"plugin_id": pluginID,
"user_id": userID,
"space_id": spaceID,
"trace_id": traceID,
"timestamp": time.Now().Unix(),
"before": before,
"after": after,
"plugin_name": after["plugin_name"],
}
logs.CtxInfof(ctx, "Plugin edit audit log: %+v", auditLog)
}
插件编辑日志内容规范:
- 请求日志:记录用户ID、工作空间ID、插件ID、插件名称、编辑操作类型、TraceID
- 锁定日志:记录锁定获取、释放、续期操作,锁定拥有者、锁定过期时间
- 版本日志:记录版本验证、版本冲突、版本更新等版本相关操作
- 业务日志:记录插件编辑步骤、参数验证结果、权限验证结果、数据更新过程
- 性能日志:记录编辑接口响应时间、锁定获取时间、数据库更新时间、ES索引更新时间
- 错误日志:记录编辑错误堆栈、锁定冲突详情、版本冲突详情、失败原因分析
- 审计日志:记录插件的编辑操作、修改前后内容对比、编辑结果、关联的工具信息
- 安全日志:记录编辑频率、权限验证、参数验证、可疑编辑行为
9.4 插件编辑监控和告警
插件编辑关键指标监控:
- 编辑性能:插件编辑响应时间、编辑成功率、编辑QPS、编辑吞吐量
- 锁定指标:锁定获取成功率、锁定冲突次数、锁定过期次数、锁定续期成功率
- 版本指标:版本冲突次数、版本验证失败次数、乐观锁机制触发次数
- 资源使用:插件数据库连接数、ES索引更新延迟、内存使用率、Redis锁定服务延迟
- 业务指标:插件编辑成功率、编辑频率分布、不同类型编辑操作比例、用户编辑活跃度
- 安全指标:权限验证通过率、恶意编辑尝试次数、编辑频率限制触发次数、参数验证失败率
- 质量指标:插件清单验证通过率、OpenAPI文档验证通过率、编辑冲突解决率
插件编辑告警策略:
- 编辑失败率告警:当插件编辑失败率超过5%时触发告警
- 锁定冲突告警:当检测到大量锁定冲突时(>10次/分钟)触发告警
- 版本冲突告警:当版本冲突次数超过预期阈值时触发告警
- 性能告警:当插件编辑响应时间超过3秒时触发告警
- 资源告警:当插件数据库连接数超过80%时触发告警
- 安全告警:当检测到异常编辑行为时立即触发告警
- 数据一致性告警:当MySQL和ES编辑状态不一致时触发告警
- 锁定服务告警:当锁定服务异常或响应超时超过200ms时触发告警
go
// 插件编辑监控指标收集
type PluginEditMetrics struct {
EditSuccessCount int64 // 编辑成功次数
EditFailureCount int64 // 编辑失败次数
EditLatency time.Duration // 编辑延迟
LockAcquireSuccessCount int64 // 锁定获取成功次数
LockAcquireFailedCount int64 // 锁定获取失败次数
LockExpireCount int64 // 锁定过期次数
LockRenewSuccessCount int64 // 锁定续期成功次数
LockRenewFailedCount int64 // 锁定续期失败次数
VersionConflictCount int64 // 版本冲突次数
PermissionDeniedCount int64 // 权限拒绝次数
RateLimitCount int64 // 频率限制次数
ParameterValidationFailCount int64 // 参数验证失败次数
IndexUpdateLatency time.Duration // 索引更新延迟
ToolsEditedCount int64 // 工具编辑次数
ManifestValidationFailCount int64 // 清单验证失败次数
OpenapiValidationFailCount int64 // OpenAPI验证失败次数
EditFieldChangeCount map[string]int64 // 各字段编辑次数统计
EventPublishLatency time.Duration // 事件发布延迟
DatabaseUpdateLatency time.Duration // 数据库更新延迟
LockServiceLatency time.Duration // 锁定服务延迟
}
// 插件编辑监控指标上报
func (s *PluginApplicationService) reportEditMetrics(ctx context.Context, operation string, startTime time.Time, pluginID int64, req *pluginAPI.UpdatePluginMetaRequest, err error) {
latency := time.Since(startTime)
if err != nil {
metrics.EditFailureCount++
// 根据错误类型分类统计
switch {
case errors.Is(err, errno.ErrPluginPermissionCode):
metrics.PermissionDeniedCount++
case errors.Is(err, errno.ErrPluginEditRateLimit):
metrics.RateLimitCount++
case errors.Is(err, errno.ErrPluginInvalidParamCode):
metrics.ParameterValidationFailCount++
case errors.Is(err, errno.ErrPluginInvalidManifest):
metrics.ManifestValidationFailCount++
case errors.Is(err, errno.ErrPluginInvalidOpenapi3Doc):
metrics.OpenapiValidationFailCount++
case errors.Is(err, errno.ErrPluginVersionConflict):
metrics.VersionConflictCount++
case errors.Is(err, errno.ErrPluginLockAcquireFailed):
metrics.LockAcquireFailedCount++
case errors.Is(err, errno.ErrPluginLockExpired):
metrics.LockExpireCount++
case errors.Is(err, errno.ErrPluginLockRenewFailed):
metrics.LockRenewFailedCount++
}
logs.CtxErrorf(ctx, "Plugin %s failed, pluginID=%d, pluginName=%s, spaceID=%d, error=%v, latency=%dms",
operation, pluginID, req.GetName(), req.GetSpaceID(), err, latency.Milliseconds())
} else {
metrics.EditSuccessCount++
metrics.EditLatency = latency
// 记录编辑字段统计
if req.GetName() != "" {
metrics.EditFieldChangeCount["name"]++
}
if req.GetDesc() != "" {
metrics.EditFieldChangeCount["desc"]++
}
if req.GetURL() != "" {
metrics.EditFieldChangeCount["server_url"]++
}
if req.Icon != nil && req.Icon.URI != "" {
metrics.EditFieldChangeCount["icon"]++
}
if req.AuthType != nil {
metrics.EditFieldChangeCount["auth_type"]++
}
logs.CtxInfof(ctx, "Plugin %s succeeded, pluginID=%d, pluginName=%s, spaceID=%d, latency=%dms",
operation, pluginID, req.GetName(), req.GetSpaceID(), latency.Milliseconds())
}
// 上报到监控系统
s.metricsReporter.Report(ctx, "plugin_edit", map[string]interface{}{
"operation": operation,
"plugin_id": pluginID,
"plugin_name": req.GetName(),
"space_id": req.GetSpaceID(),
"success": err == nil,
"latency_ms": latency.Milliseconds(),
"error_type": getEditErrorType(err),
"lock_status": ctx.Value("lock_status"),
"version": req.GetVersion(),
})
}
// 获取编辑错误类型
func getEditErrorType(err error) string {
if err == nil {
return "none"
}
// 基于真实错误码定义:backend/types/errno/plugin.go
switch {
case errors.Is(err, errno.ErrPluginPermissionCode):
return "permission_denied"
case errors.Is(err, errno.ErrPluginInvalidManifest):
return "invalid_manifest"
case errors.Is(err, errno.ErrPluginInvalidOpenapi3Doc):
return "invalid_openapi_doc"
case errors.Is(err, errno.ErrPluginEditConflict):
return "edit_conflict"
case errors.Is(err, errno.ErrPluginLockAcquireFailed):
return "lock_acquire_failed"
case errors.Is(err, errno.ErrPluginLockExpired):
return "lock_expired"
case errors.Is(err, errno.ErrPluginVersionConflict):
return "version_conflict"
case errors.Is(err, errno.ErrPluginRecordNotFound):
return "plugin_not_found"
case errors.Is(err, errno.ErrToolEditFailed):
return "tool_edit_failed"
default:
return "system_error"
}
}