From 68418056538f08c9d1e861ac2526e28e0d9de6be Mon Sep 17 00:00:00 2001 From: yokowu <18836617@qq.com> Date: Tue, 27 Oct 2026 12:25:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=94=99=E8=AF=AF=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E4=B8=A2=E5=A4=B1=E5=B9=B6=E7=A7=BB=E9=99=A4=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=81=9A=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- monkeyai/.env.example | 3 - monkeyai/backend/cmd/server/main.go | 14 +- monkeyai/backend/docs/logging-validation.md | 12 + monkeyai/backend/docs/logging.md | 19 +- monkeyai/backend/internal/apikey/agent.go | 4 +- .../backend/internal/apikey/service_test.go | 2 +- monkeyai/backend/internal/app/app.go | 15 +- monkeyai/backend/internal/app/billing.go | 5 + monkeyai/backend/internal/app/design.go | 2 + monkeyai/backend/internal/audit/middleware.go | 6 + monkeyai/backend/internal/audit/service.go | 13 +- monkeyai/backend/internal/billing/admin.go | 26 +- .../backend/internal/billing/connection.go | 17 + monkeyai/backend/internal/billing/query.go | 4 +- monkeyai/backend/internal/billing/wallet.go | 4 +- monkeyai/backend/internal/config/config.go | 3 + monkeyai/backend/internal/config/flags.go | 37 +- .../backend/internal/config/logging_test.go | 42 +-- .../backend/internal/designcatalog/api.go | 15 +- .../backend/internal/designcatalog/decode.go | 4 + .../backend/internal/designcatalog/digest.go | 24 +- .../backend/internal/designcatalog/files.go | 45 ++- .../backend/internal/designcatalog/inspect.go | 11 +- .../backend/internal/designcatalog/package.go | 71 +++- .../internal/designcatalog/postgres.go | 23 +- .../backend/internal/designcatalog/query.go | 12 +- .../backend/internal/designcatalog/storage.go | 23 +- .../backend/internal/designcatalog/tokens.go | 6 + .../backend/internal/endpoint/connection.go | 7 +- .../backend/internal/endpoint/execution.go | 10 +- monkeyai/backend/internal/endpoint/handler.go | 22 +- .../backend/internal/endpoint/postgres.go | 5 +- .../backend/internal/endpoint/protocol.go | 15 + .../internal/endpoint/protocol_test.go | 9 +- .../backend/internal/endpoint/register.go | 5 + monkeyai/backend/internal/endpoint/service.go | 9 +- monkeyai/backend/internal/expert/avatar.go | 14 +- monkeyai/backend/internal/expert/service.go | 6 +- monkeyai/backend/internal/feedback/admin.go | 4 +- monkeyai/backend/internal/feedback/agent.go | 23 +- .../backend/internal/feedback/feedback.go | 6 +- monkeyai/backend/internal/group/agent.go | 4 + monkeyai/backend/internal/group/service.go | 4 +- .../backend/internal/httpapi/router_test.go | 2 +- monkeyai/backend/internal/identity/admin.go | 5 +- .../backend/internal/identity/admincreate.go | 4 + .../backend/internal/identity/callback.go | 7 + monkeyai/backend/internal/identity/consent.go | 10 + monkeyai/backend/internal/identity/device.go | 41 ++- monkeyai/backend/internal/identity/email.go | 8 +- monkeyai/backend/internal/identity/grant.go | 19 +- .../backend/internal/identity/middleware.go | 6 + monkeyai/backend/internal/identity/oauth.go | 14 +- .../backend/internal/identity/password.go | 18 +- monkeyai/backend/internal/identity/reset.go | 9 +- monkeyai/backend/internal/identity/search.go | 6 +- monkeyai/backend/internal/identity/service.go | 5 + .../backend/internal/identity/upstream.go | 8 +- .../internal/identity/upstream_test.go | 10 +- monkeyai/backend/internal/imagegen/inputs.go | 10 +- .../internal/imagegen/openai/images/images.go | 3 + .../imagegen/openai/responses/responses.go | 3 + monkeyai/backend/internal/imagegen/outputs.go | 10 +- .../backend/internal/imagegen/postgres.go | 5 +- monkeyai/backend/internal/imagegen/pricing.go | 7 + monkeyai/backend/internal/imagegen/service.go | 7 +- .../backend/internal/imagegen/service_test.go | 6 +- .../backend/internal/imagegen/upstream.go | 11 +- .../imagegen/volcengine/volcengine.go | 4 + monkeyai/backend/internal/imagegen/xai/xai.go | 7 + monkeyai/backend/internal/imageproxy/proxy.go | 11 +- monkeyai/backend/internal/mcp/credential.go | 12 +- monkeyai/backend/internal/mcp/discovery.go | 16 +- monkeyai/backend/internal/mcp/gateway.go | 15 +- monkeyai/backend/internal/mcp/icon.go | 11 +- monkeyai/backend/internal/mcp/oauth.go | 37 +- monkeyai/backend/internal/mcp/oauth_test.go | 4 +- monkeyai/backend/internal/mcp/proxy.go | 4 + monkeyai/backend/internal/mcp/registration.go | 8 +- monkeyai/backend/internal/mcp/remote.go | 10 +- monkeyai/backend/internal/mcp/service.go | 17 +- monkeyai/backend/internal/mcp/tools.go | 3 + monkeyai/backend/internal/mcp/transport.go | 2 + monkeyai/backend/internal/model/admin.go | 8 +- monkeyai/backend/internal/model/image.go | 10 + monkeyai/backend/internal/model/page.go | 6 +- monkeyai/backend/internal/model/service.go | 10 +- monkeyai/backend/internal/model/video.go | 23 +- monkeyai/backend/internal/proxy/billing.go | 6 +- monkeyai/backend/internal/proxy/encoding.go | 4 + monkeyai/backend/internal/proxy/estimate.go | 21 +- .../backend/internal/proxy/observation.go | 1 - .../internal/proxy/observation_test.go | 7 +- monkeyai/backend/internal/proxy/proxy.go | 6 + monkeyai/backend/internal/proxy/proxy_test.go | 6 +- monkeyai/backend/internal/proxy/reconcile.go | 6 +- .../backend/internal/proxy/reconcile_test.go | 2 +- monkeyai/backend/internal/proxy/transport.go | 4 + monkeyai/backend/internal/proxy/usage_test.go | 2 +- monkeyai/backend/internal/relay/bridge.go | 47 ++- monkeyai/backend/internal/relay/connection.go | 29 +- monkeyai/backend/internal/relay/directory.go | 6 + monkeyai/backend/internal/relay/file.go | 35 +- monkeyai/backend/internal/relay/identity.go | 27 +- monkeyai/backend/internal/relay/lease.go | 74 +++- monkeyai/backend/internal/relay/preview.go | 17 +- monkeyai/backend/internal/relay/protocol.go | 9 + monkeyai/backend/internal/relay/proxy.go | 9 +- monkeyai/backend/internal/relay/service.go | 28 +- monkeyai/backend/internal/relay/signing.go | 10 + monkeyai/backend/internal/relay/stream.go | 17 +- monkeyai/backend/internal/relay/terminal.go | 7 + monkeyai/backend/internal/resource/grants.go | 10 +- monkeyai/backend/internal/resource/page.go | 4 + monkeyai/backend/internal/resource/store.go | 6 + monkeyai/backend/internal/resource/tags.go | 2 + .../internal/resourceimport/artifact.go | 7 +- .../backend/internal/resourceimport/design.go | 6 +- .../backend/internal/resourceimport/load.go | 34 +- .../internal/resourceimport/package.go | 15 +- .../backend/internal/resourceimport/reader.go | 21 +- .../internal/resourceimport/service.go | 24 +- .../backend/internal/resourceimport/upload.go | 60 ++- monkeyai/backend/internal/session/agent.go | 3 + monkeyai/backend/internal/session/decode.go | 9 +- monkeyai/backend/internal/session/index.go | 3 + monkeyai/backend/internal/session/turns.go | 18 +- monkeyai/backend/internal/setting/admin.go | 4 +- monkeyai/backend/internal/setting/email.go | 33 +- .../backend/internal/setting/email_test.go | 4 +- monkeyai/backend/internal/setting/proxy.go | 10 + monkeyai/backend/internal/setting/service.go | 29 +- monkeyai/backend/internal/skill/package.go | 10 +- monkeyai/backend/internal/skill/service.go | 11 +- .../backend/internal/stats/conversations.go | 8 + monkeyai/backend/internal/stats/history.go | 6 +- .../internal/stats/reporting_detail.go | 4 + .../internal/stats/reporting_filter.go | 29 +- .../backend/internal/testmember/member.go | 13 +- monkeyai/backend/internal/videogen/cleanup.go | 7 +- .../internal/videogen/diagnostics_test.go | 3 +- monkeyai/backend/internal/videogen/http.go | 29 +- monkeyai/backend/internal/videogen/inputs.go | 19 +- monkeyai/backend/internal/videogen/outputs.go | 24 +- .../backend/internal/videogen/provider.go | 5 + monkeyai/backend/internal/videogen/service.go | 42 ++- monkeyai/backend/internal/videoproxy/proxy.go | 39 +- monkeyai/backend/logging/benchmark_test.go | 8 +- monkeyai/backend/logging/context.go | 6 +- monkeyai/backend/logging/dedup.go | 343 ------------------ monkeyai/backend/logging/dedup_test.go | 162 --------- monkeyai/backend/logging/error.go | 10 +- monkeyai/backend/logging/error_test.go | 19 +- monkeyai/backend/logging/http.go | 14 +- monkeyai/backend/logging/http_test.go | 18 +- monkeyai/backend/logging/lifecycle_test.go | 58 --- monkeyai/backend/logging/logger.go | 34 +- monkeyai/backend/logging/logger_test.go | 119 +++++- monkeyai/backend/logging/policy.go | 77 ---- monkeyai/backend/logging/safe.go | 11 +- monkeyai/backend/logging/worker.go | 3 - monkeyai/backend/logging/worker_test.go | 38 +- monkeyai/backend/server/reset.go | 6 +- monkeyai/backend/server/reset_test.go | 2 +- monkeyai/backend/server/server.go | 8 - .../design/error-logging-optimization-plan.md | 2 + monkeyai/docker-compose.yml | 3 - 167 files changed, 1803 insertions(+), 1126 deletions(-) delete mode 100644 monkeyai/backend/logging/dedup.go delete mode 100644 monkeyai/backend/logging/dedup_test.go delete mode 100644 monkeyai/backend/logging/lifecycle_test.go delete mode 100644 monkeyai/backend/logging/policy.go diff --git a/monkeyai/.env.example b/monkeyai/.env.example index f92021853..9cb802080 100644 --- a/monkeyai/.env.example +++ b/monkeyai/.env.example @@ -2,9 +2,6 @@ POSTGRES_PASSWORD=change-this-database-password # 去重关闭后恢复逐事件输出,安全过滤和请求关联继续生效。 MONKEYAI_LOG_LEVEL=warn -MONKEYAI_LOG_DEDUP_ENABLED=true -MONKEYAI_LOG_DEDUP_WINDOW=60s -MONKEYAI_LOG_DEDUP_MAX_KEYS=4096 MONKEYAI_LOG_MAX_SIZE=20m MONKEYAI_LOG_MAX_FILES=5 MONKEYAI_INITIAL_ADMIN_EMAIL=admin@example.com diff --git a/monkeyai/backend/cmd/server/main.go b/monkeyai/backend/cmd/server/main.go index 44ae70f8c..58aca6aa5 100644 --- a/monkeyai/backend/cmd/server/main.go +++ b/monkeyai/backend/cmd/server/main.go @@ -28,22 +28,18 @@ func mainCode() int { } return 0 } - level, settings, err := config.ParseLogging(os.Args[1:]) + level, err := config.ParseLogLevel(os.Args[1:]) if errors.Is(err, flag.ErrHelp) { config.PrintUsage(os.Stdout) return 0 } if err != nil { - _, _ = os.Stderr.WriteString("启动参数无效,请使用 --help 查看用法\n") - return 2 - } - logs, err := logging.NewRuntime(os.Stdout, level, settings) - if err != nil { - _, _ = os.Stderr.WriteString("日志配置无效\n") + if _, err := os.Stderr.WriteString("启动参数无效,请使用 --help 查看用法\n"); err != nil { + slog.Error("写入输出失败", "module", "main", "operation", "mainCode", "error", err) + } return 2 } - defer logs.Close() - logger := logs.Logger() + logger := logging.New(os.Stdout, level) slog.SetDefault(logger) if err := run(logger); err != nil { logger.Error("服务退出失败", "error", err) diff --git a/monkeyai/backend/docs/logging-validation.md b/monkeyai/backend/docs/logging-validation.md index 8a3b828c9..9fc364bcf 100644 --- a/monkeyai/backend/docs/logging-validation.md +++ b/monkeyai/backend/docs/logging-validation.md @@ -1,9 +1,21 @@ # 后端日志实施验收记录 +> 后续修订:已移除日志去重、定时摘要及 Runtime,入口使用 `logging.New`,错误原文写入 `error_message`。下文保留原方案及历史验收记录,当前行为以[运行日志说明](logging.md)为准。 + 日期:2026-10-20;范围调整:2026-10-21。对应[设计方案](../../design/error-logging-optimization-plan.md)与[运行日志说明](logging.md)。本记录描述本地实现和测试,不代表镜像已发布或已部署。 本次范围调整暂缓 Nginx 日志改造,已还原两份 Nginx 配置并移除新增的 `TestNginxSafeLogging`。下述完整回归数字保留为调整前的历史记录,不将此前 Nginx 日志验证结果作为当前版本的验收依据。 +## 移除聚合后的验证 + +本轮删除聚合运行时和配置,并将错误处理调整为向上返回或记录原始原因,覆盖响应输出、存储清理、事务回滚、后台任务和日志输出失败。 + +- 后端与 Pro:`go test -race ./...`、`go vet ./...`、`go run github.com/kisielk/errcheck@v1.30.0 -blank -ignoretests ./...` 通过。 +- 安装包:`go test ./pkg/installer/deploy` 通过,确保 Compose 和环境模板不再下发去重配置。 +- 后端:`go run ./tools/sqlcschema -check` 通过,迁移编号无冲突,API 与迁移文件未修改。 +- 回归覆盖并发重复事件逐条保留、错误链和长文本、HTTP 运行时及 panic 原文、日志输出失败的 stderr 回退。 +- 当前环境未提供专项数据库、对象存储、浏览器和容器配置,相应集成测试按既有条件跳过;下文历史集成结果不作为本轮验证证据。 + ## 实现结果 - 公共安全 Handler、错误链分类、原始源码位置、根 logger 注入与共享动态阈值。 diff --git a/monkeyai/backend/docs/logging.md b/monkeyai/backend/docs/logging.md index 5dc7ecc20..9122a1b1e 100644 --- a/monkeyai/backend/docs/logging.md +++ b/monkeyai/backend/docs/logging.md @@ -11,24 +11,23 @@ | 环境变量 | 默认值 | 约束 | | --- | --- | --- | | `MONKEYAI_LOG_LEVEL` | `warn` | 启动参数 `--log-level` 优先 | -| `MONKEYAI_LOG_DEDUP_ENABLED` | `true` | 布尔值 | -| `MONKEYAI_LOG_DEDUP_WINDOW` | `60s` | 1 秒至 10 分钟 | -| `MONKEYAI_LOG_DEDUP_MAX_KEYS` | `4096` | 128 至 65536 | -未设置环境变量和启动参数时,默认级别为 WARN;显式设置 `info` 或 `debug` 仍按指定级别生效。格式或范围无效会使入口启动失败,不回显配置值。关闭去重仍保留安全过滤、错误分类和请求关联。分钟级后台扫描的有效窗口至少为 5 分钟,实际重试节奏保持不变。 +未设置环境变量和启动参数时,默认级别为 WARN;显式设置 `info` 或 `debug` 仍按指定级别生效。无效日志级别会使入口启动失败,不回显配置值。旧的 `MONKEYAI_LOG_DEDUP_*` 环境变量不再读取。 -## 汇总与计数 +## 错误详情 -只有代码明确登记的重复故障参与去重。首次故障直出,窗口内后续发生次数进入 `logging.error.summary`;没有后续请求时也由定时器刷新。持续窗口只输出摘要,完整空闲窗口后再次发生会直出首条。 +所有达到当前日志级别的事件均即时逐条输出,不做去重、采样或定时汇总。根 logger 和派生 logger 使用同一输出链路,无后台日志调度器或关闭刷新要求。 -摘要中的 `occurrence_count = detail_count + suppressed_count`,只统计该窗口。摘要保留原严重级别,最多保留三个关联 ID 样本;不能通过日志行数计算错误次数。关联 ID、时间及错误全文不参与指纹。任务与交易重试按对象隔离,上传块按上传 ID、块序号和阶段隔离。最终失败、审计失败、补偿和状态事件默认直出。 +`ErrorAttrs` 和 `Diagnostic` 保留完整的 `error_message`,同时输出 `error_code`、`error_type` 等分类字段。直接传入 `"error", err` 时,这些字段位于 `error` 对象内;传入错误字符串时保留该字符串。错误文本保留包装上下文、中文、多行及长消息,不截断。依赖检查错误保留依赖名和原始原因,HTTP 运行时错误与 panic 也记录原始文本。 -调用方可读取 `logging.Runtime.Snapshot()` 查看事件、压制、摘要、容量溢出、输出失败和 HTTP 完成计数。容量满时新指纹保守直出,不继续扩张内存。依赖恢复由实际执行检查的 worker 判断,先结算旧故障摘要,再输出一次恢复事件。 +普通属性仍过滤凭据、请求体、响应体和任意对象;错误文本本身按原文记录,可能包含错误来源携带的 URL、路径或其他信息。HTTP 错误响应仍使用原有通用文案,日志详情不会自动写入响应。 -根 logger 的创建方在业务与其他模块全部结束、最终退出事件提交后调用 `Runtime.Close()`。关闭幂等,迟到事件直出;stdout 仍由创建方持有。强杀、进程崩溃及输出故障可能丢失日志,业务证据以原有审计和持久化状态为准。 +后台依赖检查每次失败都记录具体错误,实际检查恢复后输出一次恢复事件。请求 ID、源码位置、动态日志级别及审计关联继续保留。 + +错误处理遵循“向上返回或记录原始错误”的原则。异步任务、计费补偿、状态更新、事务回滚、流关闭、临时文件清理及响应写入均检查返回值;需要转换为客户端固定提示或继续降级处理时,先记录原始原因。事务已结束后的兜底回滚和已清理的临时文件按正常状态识别。日志输出自身失败时尝试写入 stderr,避免静默丢失输出故障。 ## 部署 -Compose 为各服务使用 `json-file` 轮转,默认每文件 20 MiB、保留 5 个文件,可通过 `MONKEYAI_LOG_MAX_SIZE`、`MONKEYAI_LOG_MAX_FILES` 调整。现有容器需要重建才能采用新的 Docker 日志驱动设置。多副本各自去重,容量预算还需乘以实例数量。 +Compose 为各服务使用 `json-file` 轮转,默认每文件 20 MiB、保留 5 个文件,可通过 `MONKEYAI_LOG_MAX_SIZE`、`MONKEYAI_LOG_MAX_FILES` 调整。现有容器需要重建才能采用新的 Docker 日志驱动设置。重复故障会逐条写入容器日志,日志保留量由轮转配置控制。 Nginx 日志调整暂缓,沿用本次 PR 之前的 access/error 日志配置;本次后端日志改造不改变 Nginx 的输出格式、开关和日志诊断范围。 diff --git a/monkeyai/backend/internal/apikey/agent.go b/monkeyai/backend/internal/apikey/agent.go index a61ae2b2b..715833eea 100644 --- a/monkeyai/backend/internal/apikey/agent.go +++ b/monkeyai/backend/internal/apikey/agent.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/go-chi/chi/v5" ) @@ -33,6 +34,7 @@ func (s *Service) create(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(&input); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "apikey", "operation", "create", "error", err) keyError(r.Context(), w, http.StatusBadRequest, "请求格式无效") return } diff --git a/monkeyai/backend/internal/apikey/service_test.go b/monkeyai/backend/internal/apikey/service_test.go index 0a24fe81f..3ec7425bb 100644 --- a/monkeyai/backend/internal/apikey/service_test.go +++ b/monkeyai/backend/internal/apikey/service_test.go @@ -101,7 +101,7 @@ func TestRotateLogsFailedCompensationWithoutSecret(t *testing.T) { if err == nil || len(store.revoked) != 2 || store.revoked[0] != "key-1" || store.revoked[1] != "key-2" { t.Fatalf("撤销失败后应尝试回收新密钥: %v, %v", store.revoked, err) } - if !strings.Contains(output.String(), "error.error_code=internal_error") || strings.Contains(output.String(), "cleanup failed") || !strings.Contains(output.String(), "key-2") || strings.Contains(output.String(), store.hash) { + if !strings.Contains(output.String(), "error.error_code=internal_error") || !strings.Contains(output.String(), "cleanup failed") || !strings.Contains(output.String(), "key-2") || strings.Contains(output.String(), store.hash) { t.Fatalf("补偿日志缺少错误或包含密钥摘要: %s", output.String()) } } diff --git a/monkeyai/backend/internal/app/app.go b/monkeyai/backend/internal/app/app.go index f019c1a1b..6756ecee1 100644 --- a/monkeyai/backend/internal/app/app.go +++ b/monkeyai/backend/internal/app/app.go @@ -158,7 +158,9 @@ func debugHandler(runtime *relay.Service) http.Handler { mux.HandleFunc("GET /debug/relay", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") - json.NewEncoder(w).Encode(runtime.Metrics()) + if err := json.NewEncoder(w).Encode(runtime.Metrics()); err != nil { + slog.Error("写入输出失败", "module", "app", "operation", "debugHandler", "error", err) + } }) } return mux @@ -339,6 +341,7 @@ func newApplicationHandlerWithMembers(ctx context.Context, logger *slog.Logger, ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := keys.RevokeRuntimeKeys(ctx, id); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "app", "operation", "newApplicationHandlerWithMembers", "error", err) logging.Event(ctx, logger, slog.LevelWarn, "app.operation.warn", "撤销独立执行密钥标记失败", "module", "app", "operation", "newApplicationHandlerWithMembers", "grant_id", id, "reason", "storage_unavailable") } }) @@ -411,6 +414,9 @@ func (a *App) Run(ctx context.Context) error { case <-cleanupKeys.C: ctx, cancel := context.WithTimeout(workerCtx, 5*time.Second) if err := a.runtimeKeys.CleanupRuntimeKeys(ctx); err != nil && workerCtx.Err() == nil { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "app", "operation", "Run", "error", err) + } logging.Event(ctx, slog.Default(), slog.LevelWarn, "worker.scan.failed", "回收独立执行密钥失败", "module", "app", "operation", "Run", "dependency", "storage", "reason", "storage_unavailable") } cancel() @@ -434,7 +440,12 @@ func (a *App) Run(ctx context.Context) error { cleanupDone := make(chan struct{}) go func() { defer close(cleanupDone); runImageCleanup(workerCtx, a.inputs) }() feedbackCleanupDone := make(chan struct{}) - go func() { defer close(feedbackCleanupDone); _ = a.feedback.Run(workerCtx, time.Hour) }() + go func() { + defer close(feedbackCleanupDone) + if err := a.feedback.Run(workerCtx, time.Hour); err != nil { + slog.Error("操作失败", "module", "app", "operation", "Run", "error", err) + } + }() uploadCleanupDone := make(chan struct{}) go func() { defer close(uploadCleanupDone) diff --git a/monkeyai/backend/internal/app/billing.go b/monkeyai/backend/internal/app/billing.go index 2dad5ae0b..a3c95af40 100644 --- a/monkeyai/backend/internal/app/billing.go +++ b/monkeyai/backend/internal/app/billing.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "log/slog" "math" "net/http" @@ -115,6 +116,7 @@ func (r modelUsageReconciler) Reconcile(ctx context.Context, request billing.Rec func (b modelBilling) Begin(ctx context.Context, t proxy.Target, r proxy.BillingRequest) (proxy.Reservation, error) { var data map[string]json.RawMessage if err := json.Unmarshal(r.Body, &data); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "app", "operation", "Begin", "error", err) return proxy.Reservation{}, resource.Invalid("模型请求无效") } for _, key := range []string{"n", "best_of"} { @@ -136,6 +138,9 @@ func (b modelBilling) Begin(ctx context.Context, t proxy.Target, r proxy.Billing if raw, ok := data[k]; ok { var v int64 if err := json.Unmarshal(raw, &v); err != nil || v <= 0 { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "app", "operation", "Begin", "error", err) + } return proxy.Reservation{}, resource.Invalid("输出限制须为正整数") } if limit == 0 || v < limit { diff --git a/monkeyai/backend/internal/app/design.go b/monkeyai/backend/internal/app/design.go index 7a950e57a..1c3f71fcf 100644 --- a/monkeyai/backend/internal/app/design.go +++ b/monkeyai/backend/internal/app/design.go @@ -2,6 +2,7 @@ package app import ( "context" + "log/slog" "uuid" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/designcatalog" @@ -12,6 +13,7 @@ type designDevices struct{ store *endpoint.Postgres } func (d designDevices) OwnedDevice(ctx context.Context, user, machine string) (string, error) { if _, err := uuid.Parse(machine); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "app", "operation", "OwnedDevice", "error", err) return "", &designcatalog.Fault{Status: 403, Code: "forbidden", Message: "设备标识无效"} } row, err := d.store.Get(ctx, user, machine) diff --git a/monkeyai/backend/internal/audit/middleware.go b/monkeyai/backend/internal/audit/middleware.go index f97839476..538602c43 100644 --- a/monkeyai/backend/internal/audit/middleware.go +++ b/monkeyai/backend/internal/audit/middleware.go @@ -111,6 +111,9 @@ func clientIP(r *http.Request) string { remote := sourceIP(r.RemoteAddr) peer, err := netip.ParseAddr(remote) if err != nil || !peer.IsPrivate() { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "audit", "operation", "clientIP", "error", err) + } return remote } // 默认部署由内网 Nginx 转发;公网或本机直连不信任代理头。 @@ -120,6 +123,7 @@ func clientIP(r *http.Request) string { } ip, err := netip.ParseAddr(values[0]) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "audit", "operation", "clientIP", "error", err) return remote } return ip.WithZone("").Unmap().String() @@ -128,10 +132,12 @@ func clientIP(r *http.Request) string { func sourceIP(remote string) string { host, _, err := net.SplitHostPort(remote) if err != nil { + slog.Warn("操作失败", "module", "audit", "operation", "sourceIP", "error", err) host = remote } ip, err := netip.ParseAddr(host) if err != nil { + slog.Warn("操作失败", "module", "audit", "operation", "sourceIP", "error", err) return "" } return ip.WithZone("").Unmap().String() diff --git a/monkeyai/backend/internal/audit/service.go b/monkeyai/backend/internal/audit/service.go index 8961f0408..c49bf2bd6 100644 --- a/monkeyai/backend/internal/audit/service.go +++ b/monkeyai/backend/internal/audit/service.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "slices" @@ -13,6 +12,8 @@ import ( "strings" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/audit/sqlc" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5/pgtype" @@ -71,6 +72,9 @@ func optional(value string) *string { func uuid(value string) *string { var id pgtype.UUID if err := id.Scan(value); err != nil || !id.Valid { + if err != nil { + slog.Warn("操作失败", "module", "audit", "operation", "uuid", "error", err) + } return nil } return new(value) @@ -97,12 +101,18 @@ func filters(r *http.Request) (sqlc.PageParams, int, error) { if q.Has("page") { page, err = strconv.Atoi(q.Get("page")) if err != nil || page < 1 || page > 1000000 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "audit", "operation", "filters", "error", err) + } return in, 0, invalid } } if q.Has("page_size") { size, err = strconv.Atoi(q.Get("page_size")) if err != nil || size < 1 || size > 500 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "audit", "operation", "filters", "error", err) + } return in, 0, invalid } } @@ -111,6 +121,7 @@ func filters(r *http.Request) (sqlc.PageParams, int, error) { if q.Has(key) { at, err := time.Parse(time.RFC3339, q.Get(key)) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "audit", "operation", "filters", "error", err) return in, 0, invalid } *target = &at diff --git a/monkeyai/backend/internal/billing/admin.go b/monkeyai/backend/internal/billing/admin.go index c4f7497a1..b99ba8a3a 100644 --- a/monkeyai/backend/internal/billing/admin.go +++ b/monkeyai/backend/internal/billing/admin.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "slices" "strconv" @@ -14,7 +15,6 @@ import ( "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/billing/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" - "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -118,6 +118,9 @@ func (s *Service) saveSettings(w http.ResponseWriter, r *http.Request) { case "pricing": for key, target := range map[string]*Amount{"input_credits_per_million_tokens": &p.Input, "cached_input_credits_per_million_tokens": &p.Cached, "output_credits_per_million_tokens": &p.Output} { if err = json.Unmarshal(in[key], target); err != nil || *target < 0 || *target > Amount(1_000_000*scale) { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "billing", "operation", "saveSettings", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("单价须为 0 到 1000000 的积分值,最多六位小数")) return } @@ -266,6 +269,9 @@ func (s *Service) saveQuotas(w http.ResponseWriter, r *http.Request) { seen[v.Type+v.ID] = true var amount *Amount if err = json.Unmarshal(v.Credits, &amount); err != nil || amount != nil && (*amount < 0 || int64(*amount)%scale != 0) { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "billing", "operation", "saveQuotas", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("额度需为非负整数积分或 null")) return } @@ -337,6 +343,9 @@ func normalizeResetIDs(ids []string, field string) ([]string, error) { for _, id := range ids { var value pgtype.UUID if err := value.Scan(id); err != nil || !value.Valid { + if err != nil { + slog.Warn("操作失败", "module", "billing", "operation", "normalizeResetIDs", "error", err) + } return nil, resource.Invalid(field + " 必须包含有效的 UUID") } normalized = append(normalized, value.String()) @@ -372,6 +381,9 @@ func (s *Service) resetQuotas(w http.ResponseWriter, r *http.Request) { } var idempotencyID pgtype.UUID if err = idempotencyID.Scan(in.IdempotencyKey); err != nil || !idempotencyID.Valid { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "billing", "operation", "resetQuotas", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("idempotency_key 必须是有效的 UUID")) return } @@ -620,12 +632,18 @@ func pageParams(r *http.Request, maxSize int) (int, int) { page, pageErr := strconv.Atoi(r.URL.Query().Get("page")) size, sizeErr := strconv.Atoi(r.URL.Query().Get("page_size")) if pageErr != nil || page < 1 { + if pageErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "billing", "operation", "pageParams", "error", pageErr) + } page = 1 } if page > 100000 { page = 100000 } if sizeErr != nil || size < 1 || size > maxSize { + if sizeErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "billing", "operation", "pageParams", "error", sizeErr) + } size = 20 } return page, size @@ -668,6 +686,9 @@ func (s *Service) refund(w http.ResponseWriter, r *http.Request) { } amount, err := ParseAmount(amountText) if err != nil || amount <= 0 || status != "settled" { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "billing", "operation", "refund", "error", err) + } resource.Fail(r.Context(), w, fail(409, "transaction_not_refundable", "仅已结算的非零本地扣款可退款")) return } @@ -698,6 +719,9 @@ func (s *Service) refund(w http.ResponseWriter, r *http.Request) { current, err := ParseAmount(balance) if err != nil || current > maxAmount-amount { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "billing", "operation", "refund", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("退款后余额超出允许范围,请先处理账户额度")) return } diff --git a/monkeyai/backend/internal/billing/connection.go b/monkeyai/backend/internal/billing/connection.go index cc2e04679..448e616f2 100644 --- a/monkeyai/backend/internal/billing/connection.go +++ b/monkeyai/backend/internal/billing/connection.go @@ -9,6 +9,7 @@ import ( "encoding/pem" "errors" "fmt" + "log/slog" "net" "net/url" "os" @@ -56,6 +57,7 @@ func WalletFromEnv() (*Wallet, error) { if raw := os.Getenv("BAIZHIYUN_APP_ID"); raw != "" { appID, err := strconv.Atoi(raw) if err != nil { + slog.Warn("操作失败", "module", "billing", "operation", "WalletFromEnv", "error", err) return nil, resource.Invalid("BAIZHIYUN_APP_ID 必须为整数") } cfg.AppID = appID @@ -94,6 +96,7 @@ func newWallet(cfg WalletConfig) (*Wallet, error) { } pair, err := tls.X509KeyPair([]byte(cfg.Certificate), []byte(cfg.PrivateKey)) if err != nil { + slog.Warn("操作失败", "module", "billing", "operation", "newWallet", "error", err) return nil, resource.Invalid("客户端证书或私钥格式无效,或证书与私钥不匹配") } block, _ := pem.Decode([]byte(cfg.PrivateKey)) @@ -102,10 +105,14 @@ func newWallet(cfg WalletConfig) (*Wallet, error) { } key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if _, ok := key.(*ecdsa.PrivateKey); err != nil || !ok { + if err != nil { + slog.Warn("操作失败", "module", "billing", "operation", "newWallet", "error", err) + } return nil, resource.Invalid("百智云私钥必须为 PKCS#8 格式的 ECDSA 私钥(BEGIN PRIVATE KEY)") } cert, err := x509.ParseCertificate(pair.Certificate[0]) if err != nil { + slog.Warn("操作失败", "module", "billing", "operation", "newWallet", "error", err) return nil, resource.Invalid("客户端证书格式无效") } if time.Now().Before(cert.NotBefore) || !time.Now().Before(cert.NotAfter) { @@ -121,6 +128,9 @@ func newWallet(cfg WalletConfig) (*Wallet, error) { } ca, err := x509.ParseCertificate(block.Bytes) if err != nil || !ca.IsCA { + if err != nil { + slog.Warn("操作失败", "module", "billing", "operation", "newWallet", "error", err) + } return nil, resource.Invalid("CA 证书无效或不具备 CA 签发用途") } if time.Now().Before(ca.NotBefore) || !time.Now().Before(ca.NotAfter) { @@ -151,6 +161,7 @@ func (s *Service) walletConfig(ctx context.Context, q resource.Queryer) (*Wallet Wallet *WalletConfig `json:"wallet"` } if err = json.Unmarshal(record.Value, &value); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "billing", "operation", "walletConfig", "error", err) return nil, errors.New("读取百智云配置失败") } return value.Wallet, nil @@ -250,6 +261,9 @@ func (s *Service) saveWallet(ctx context.Context, q resource.Queryer, in map[str func normalizeWalletURL(raw string) (string, error) { u, err := url.Parse(strings.TrimSpace(raw)) if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.Opaque != "" { + if err != nil { + slog.Warn("操作失败", "module", "billing", "operation", "normalizeWalletURL", "error", err) + } return "", errors.New("请输入完整的 HTTPS 服务地址,不含用户名或密码") } if (u.Path != "" && u.Path != "/") || u.RawPath != "" || u.RawQuery != "" || u.ForceQuery || strings.Contains(raw, "#") { @@ -259,6 +273,9 @@ func normalizeWalletURL(raw string) (string, error) { if port != "" { n, err := strconv.Atoi(port) if err != nil || n < 1 || n > 65535 { + if err != nil { + slog.Warn("操作失败", "module", "billing", "operation", "normalizeWalletURL", "error", err) + } return "", errors.New("端口必须在 1 到 65535 之间") } port = strconv.Itoa(n) diff --git a/monkeyai/backend/internal/billing/query.go b/monkeyai/backend/internal/billing/query.go index 189352e72..9f970f1e9 100644 --- a/monkeyai/backend/internal/billing/query.go +++ b/monkeyai/backend/internal/billing/query.go @@ -3,6 +3,7 @@ package billing import ( "encoding/json" "fmt" + "log/slog" "net/http" "strings" "time" @@ -10,7 +11,6 @@ import ( "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/billing/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" - "github.com/go-chi/chi/v5" ) @@ -21,6 +21,7 @@ func filters(r *http.Request) (sqlc.CountEntriesParams, error) { if v := q.Get(key); v != "" { t, err := time.Parse(time.RFC3339, v) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "billing", "operation", "filters", "error", err) return sqlc.CountEntriesParams{}, resource.Invalid("筛选时间需使用带时区的 ISO 格式") } *target = &t @@ -89,6 +90,7 @@ func (s *Service) entries(w http.ResponseWriter, r *http.Request) { func defaultEntryRange(now time.Time) (time.Time, time.Time) { zone, err := time.LoadLocation("Asia/Shanghai") if err != nil { + slog.Warn("操作失败", "module", "billing", "operation", "defaultEntryRange", "error", err) zone = time.FixedZone("Asia/Shanghai", 8*60*60) } local := now.In(zone) diff --git a/monkeyai/backend/internal/billing/wallet.go b/monkeyai/backend/internal/billing/wallet.go index 3a649ea16..063f8c6dc 100644 --- a/monkeyai/backend/internal/billing/wallet.go +++ b/monkeyai/backend/internal/billing/wallet.go @@ -5,11 +5,12 @@ import ( "crypto/rand" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "math/big" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/billing/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" @@ -261,6 +262,7 @@ func (s *Service) BindWallet(ctx context.Context, actor, user, external string) defer cancel() profile, err := wallet.Client.GetUserByID(c, external) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "billing", "operation", "BindWallet", "error", err) return fail(422, "wallet_user_invalid", "无法核实百智云用户") } if profile.ID != external { diff --git a/monkeyai/backend/internal/config/config.go b/monkeyai/backend/internal/config/config.go index 74e646657..136570594 100644 --- a/monkeyai/backend/internal/config/config.go +++ b/monkeyai/backend/internal/config/config.go @@ -118,6 +118,9 @@ func Load(args []string) (Config, error) { } parsed, err := url.Parse(cfg.PublicURL) if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.ForceQuery || parsed.Fragment != "" { + if err != nil { + slog.Warn("操作失败", "module", "config", "operation", "Load", "error", err) + } return Config{}, errors.New("public-url 必须是无凭据、查询或片段的绝对 HTTP(S) 地址") } if (cfg.InitialAdminEmail == "") != (cfg.InitialAdminPassword == "") { diff --git a/monkeyai/backend/internal/config/flags.go b/monkeyai/backend/internal/config/flags.go index 890bd06f4..06a616b9c 100644 --- a/monkeyai/backend/internal/config/flags.go +++ b/monkeyai/backend/internal/config/flags.go @@ -5,10 +5,6 @@ import ( "flag" "io" "log/slog" - "os" - "strconv" - - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "time" ) @@ -48,39 +44,12 @@ func ParseLogLevel(args []string) (slog.Level, error) { } var level slog.Level if err := level.UnmarshalText([]byte(text)); err != nil { + slog.Warn("操作失败", "module", "config", "operation", "ParseLogLevel", "error", err) return 0, errors.New("日志级别无效") } return level, nil } -// ParseLogging reads construction settings only for the logger's owner. -func ParseLogging(args []string) (slog.Level, logging.Settings, error) { - level, err := ParseLogLevel(args) - if err != nil { - return 0, logging.Settings{}, err - } - settings := logging.DefaultSettings() - if value, ok := os.LookupEnv("MONKEYAI_LOG_DEDUP_ENABLED"); ok { - settings.Enabled, err = strconv.ParseBool(value) - if err != nil { - return 0, settings, errors.New("MONKEYAI_LOG_DEDUP_ENABLED 格式无效") - } - } - if value, ok := os.LookupEnv("MONKEYAI_LOG_DEDUP_WINDOW"); ok { - settings.Window, err = time.ParseDuration(value) - if err != nil { - return 0, settings, errors.New("MONKEYAI_LOG_DEDUP_WINDOW 格式无效") - } - } - if value, ok := os.LookupEnv("MONKEYAI_LOG_DEDUP_MAX_KEYS"); ok { - settings.MaxKeys, err = strconv.Atoi(value) - if err != nil { - return 0, settings, errors.New("MONKEYAI_LOG_DEDUP_MAX_KEYS 格式无效") - } - } - return level, settings, settings.Validate() -} - // PrintUsage uses static defaults, never secrets from the current environment. func PrintUsage(output io.Writer) { cfg := Config{HTTP: HTTP{Addr: ":8080", PprofAddr: "127.0.0.1:6060", ShutdownTimeout: 10 * time.Second}, @@ -88,6 +57,8 @@ func PrintUsage(output io.Writer) { level := defaultLogLevel.String() flags := startupFlags(&cfg, &level) flags.SetOutput(output) - _, _ = io.WriteString(output, "用法: monkeyai-server [启动参数]\n monkeyai-server reset [--database-url PostgreSQL连接地址]\n\nreset: 重置 admin@baizhi.cloud 的密码;未找到时提示填写管理员邮箱,成功后输出账号密码。\n\n启动参数:\n") + if _, err := io.WriteString(output, "用法: monkeyai-server [启动参数]\n monkeyai-server reset [--database-url PostgreSQL连接地址]\n\nreset: 重置 admin@baizhi.cloud 的密码;未找到时提示填写管理员邮箱,成功后输出账号密码。\n\n启动参数:\n"); err != nil { + slog.Error("写入输出失败", "module", "config", "operation", "PrintUsage", "error", err) + } flags.PrintDefaults() } diff --git a/monkeyai/backend/internal/config/logging_test.go b/monkeyai/backend/internal/config/logging_test.go index c519dafcf..cb18f5e9f 100644 --- a/monkeyai/backend/internal/config/logging_test.go +++ b/monkeyai/backend/internal/config/logging_test.go @@ -1,48 +1,20 @@ package config import ( - "errors" - "flag" "log/slog" - "strings" "testing" - "time" ) -func TestParseLoggingSettings(t *testing.T) { - t.Setenv("MONKEYAI_LOG_DEDUP_ENABLED", "true") - t.Setenv("MONKEYAI_LOG_DEDUP_WINDOW", "60s") - t.Setenv("MONKEYAI_LOG_DEDUP_MAX_KEYS", "4096") - level, settings, err := ParseLogging([]string{"--log-level=error"}) - if err != nil || level != slog.LevelError || !settings.Enabled || settings.Window != time.Minute || settings.MaxKeys != 4096 { - t.Fatalf("日志设置错误: %+v %v", settings, err) +func TestRemovedDedupSettingsAreIgnored(t *testing.T) { + for _, name := range []string{"MONKEYAI_LOG_DEDUP_ENABLED", "MONKEYAI_LOG_DEDUP_WINDOW", "MONKEYAI_LOG_DEDUP_MAX_KEYS"} { + t.Setenv(name, "obsolete") } - for _, test := range []struct{ key, value string }{ - {"MONKEYAI_LOG_DEDUP_ENABLED", "private-secret"}, - {"MONKEYAI_LOG_DEDUP_WINDOW", "private-secret"}, - {"MONKEYAI_LOG_DEDUP_WINDOW", "0s"}, - {"MONKEYAI_LOG_DEDUP_WINDOW", "601s"}, - {"MONKEYAI_LOG_DEDUP_MAX_KEYS", "private-secret"}, - {"MONKEYAI_LOG_DEDUP_MAX_KEYS", "127"}, - {"MONKEYAI_LOG_DEDUP_MAX_KEYS", "65537"}, - } { - t.Run(test.key+test.value, func(t *testing.T) { - t.Setenv(test.key, test.value) - _, _, err := ParseLogging(nil) - if err == nil || strings.Contains(err.Error(), "private-secret") { - t.Fatalf("无效设置未安全拒绝: %v", err) - } - _, _, err = ParseLogging([]string{"--help"}) - if !errors.Is(err, flag.ErrHelp) { - t.Fatal("帮助被无效配置阻断", err) - } - }) + level, err := ParseLogLevel([]string{"--log-level=error"}) + if err != nil || level != slog.LevelError { + t.Fatalf("日志级别解析失败: %v %v", level, err) } -} -func TestBusinessConfigDoesNotReadInjectedLoggerSettings(t *testing.T) { t.Setenv("MONKEYAI_DATABASE_URL", "postgres://unused") - t.Setenv("MONKEYAI_LOG_DEDUP_WINDOW", "private-secret") if _, err := Load(nil); err != nil { - t.Fatal("嵌入服务读取了调用方的日志构造配置", err) + t.Fatal(err) } } diff --git a/monkeyai/backend/internal/designcatalog/api.go b/monkeyai/backend/internal/designcatalog/api.go index c1f25dcc0..3d7ba81d0 100644 --- a/monkeyai/backend/internal/designcatalog/api.go +++ b/monkeyai/backend/internal/designcatalog/api.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "log/slog" "net/http" "net/url" "strings" @@ -37,6 +38,7 @@ func pathParam(r *http.Request, name string) string { if r.URL.RawPath != "" { decoded, err := url.PathUnescape(value) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "designcatalog", "operation", "pathParam", "error", err) return "" } return decoded @@ -101,6 +103,7 @@ func catalogAuth(require func(http.Handler) http.Handler) func(http.Handler) htt } } func writeAPIError(w http.ResponseWriter, r *http.Request, err error) { + slog.ErrorContext(r.Context(), "设计资源请求失败", "error", err) var f *Fault if !errors.As(err, &f) { f = &Fault{503, "unavailable", "设计目录或存储暂不可用"} @@ -113,7 +116,9 @@ func writeAPIError(w http.ResponseWriter, r *http.Request, err error) { w.Header().Del("Content-Disposition") w.WriteHeader(f.Status) if r.Method != "HEAD" { - _ = json.NewEncoder(w).Encode(map[string]any{"code": f.Code, "message": f.Message, "data": nil}) + if err := json.NewEncoder(w).Encode(map[string]any{"code": f.Code, "message": f.Message, "data": nil}); err != nil { + slog.Error("写入输出失败", "module", "designcatalog", "operation", "writeAPIError", "error", err) + } } } func apiJSON(w http.ResponseWriter, data any) { @@ -121,11 +126,16 @@ func apiJSON(w http.ResponseWriter, data any) { w.Header().Set("Cache-Control", "private, max-age=0, must-revalidate") w.Header().Set("Vary", "Authorization") w.WriteHeader(200) - _ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "message": "", "data": data}) + if err := json.NewEncoder(w).Encode(map[string]any{"code": 0, "message": "", "data": data}); err != nil { + slog.Error("写入输出失败", "module", "designcatalog", "operation", "apiJSON", "error", err) + } } func query(r *http.Request) (url.Values, error) { q, err := url.ParseQuery(r.URL.RawQuery) if err != nil || len(r.URL.RawQuery) > 16384 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "designcatalog", "operation", "query", "error", err) + } return nil, fault(400, "invalid_argument", "查询编码无效") } for key, values := range q { @@ -211,6 +221,7 @@ func (s *Service) token(w http.ResponseWriter, r *http.Request) { Target string `json:"target"` } if err := resource.Decode(w, r, &input); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "designcatalog", "operation", "token", "error", err) controlError(w, r, fault(400, "invalid_argument", "Token 请求无效")) return } diff --git a/monkeyai/backend/internal/designcatalog/decode.go b/monkeyai/backend/internal/designcatalog/decode.go index ee5319aee..d00952fd2 100644 --- a/monkeyai/backend/internal/designcatalog/decode.go +++ b/monkeyai/backend/internal/designcatalog/decode.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "log/slog" "slices" ) @@ -29,6 +30,9 @@ func validateProvenance(b json.RawMessage) error { } var fields map[string]json.RawMessage if err := decode(b, &fields); err != nil || fields == nil { + if err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "validateProvenance", "error", err) + } return errors.New("provenance 必须是对象") } for key, raw := range fields { diff --git a/monkeyai/backend/internal/designcatalog/digest.go b/monkeyai/backend/internal/designcatalog/digest.go index fa71ce314..4a84d1d19 100644 --- a/monkeyai/backend/internal/designcatalog/digest.go +++ b/monkeyai/backend/internal/designcatalog/digest.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "log/slog" "math/big" "mime" "path" @@ -67,6 +68,9 @@ func validateMetadata(e Entry) error { } var meta map[string]json.RawMessage if err := decode(e.Metadata, &meta); err != nil || meta == nil { + if err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "validateMetadata", "error", err) + } return errors.New("metadata 必须是对象") } for key, b := range meta { @@ -103,6 +107,7 @@ func validateMetadata(e Entry) error { return fmt.Errorf("metadata.%s 不接受 null", key) } if err := json.Unmarshal(b, target); err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "validateMetadata", "error", err) return fmt.Errorf("metadata.%s 类型错误", key) } } @@ -165,6 +170,7 @@ func ContentDigest(e Entry) (string, error) { return "", errors.New("files 引用无效或重复") } if _, _, err := mime.ParseMediaType(f.MIME); err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "ContentDigest", "error", err) return "", errors.New("files MIME 无效") } names[f.Path] = true @@ -244,6 +250,9 @@ func ManifestDigest(raw []byte) (string, error) { dec := json.NewDecoder(bytes.NewReader(raw)) token, err := dec.Token() if err != nil || token != json.Delim('{') { + if err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "ManifestDigest", "error", err) + } return "", errors.New("manifest 必须是对象") } var buf bytes.Buffer @@ -264,7 +273,10 @@ func ManifestDigest(raw []byte) (string, error) { if n > 0 { buf.WriteByte(',') } - k, _ := json.Marshal(key) + k, err := json.Marshal(key) + if err != nil { + return "", err + } v, err := json.Marshal(value) if err != nil { return "", err @@ -283,5 +295,11 @@ func ManifestDigest(raw []byte) (string, error) { buf.WriteByte('}') return Hash(buf.Bytes()), nil } -func stringValue(raw json.RawMessage) string { var s string; _ = json.Unmarshal(raw, &s); return s } -func boolValue(raw json.RawMessage) bool { return bytes.Equal(raw, []byte("true")) } +func stringValue(raw json.RawMessage) string { + var s string + if err := json.Unmarshal(raw, &s); err != nil { + slog.Error("解析数据失败", "module", "designcatalog", "operation", "stringValue", "error", err) + } + return s +} +func boolValue(raw json.RawMessage) bool { return bytes.Equal(raw, []byte("true")) } diff --git a/monkeyai/backend/internal/designcatalog/files.go b/monkeyai/backend/internal/designcatalog/files.go index 23dc3d40a..254e93cfa 100644 --- a/monkeyai/backend/internal/designcatalog/files.go +++ b/monkeyai/backend/internal/designcatalog/files.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io" + "log/slog" "mime" "mime/multipart" "net/http" @@ -47,12 +48,18 @@ func ranges(raw string, size int64) ([]byteRange, error) { } else { start, err = strconv.ParseInt(pair[0], 10, 64) if err != nil || start < 0 { + if err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "ranges", "error", err) + } return nil, fault(400, "invalid_argument", "Range 起点无效") } end = size - 1 if pair[1] != "" { end, err = strconv.ParseInt(pair[1], 10, 64) if err != nil || end < start { + if err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "ranges", "error", err) + } return nil, fault(400, "invalid_argument", "Range 终点无效") } } @@ -102,7 +109,10 @@ func (s *Service) serveObject(w http.ResponseWriter, r *http.Request, object Obj if filename != "" { w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename})) } - mediaType, _, _ := mime.ParseMediaType(contentType) + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil { + slog.Error("解析内容类型失败", "error", err) + } if mediaType == "image/svg+xml" { w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'") } @@ -134,9 +144,15 @@ func (s *Service) serveObject(w http.ResponseWriter, r *http.Request, object Obj writeAPIError(w, r, err) return } - defer body.Close() + defer func() { + if err := body.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "serveObject", "error", err) + } + }() w.WriteHeader(200) - _, _ = io.CopyN(w, body, object.Size) + if _, err := io.CopyN(w, body, object.Size); err != nil { + slog.Error("传输响应失败", "module", "designcatalog", "operation", "serveObject", "error", err) + } return } if len(selected) == 1 { @@ -153,15 +169,23 @@ func (s *Service) serveObject(w http.ResponseWriter, r *http.Request, object Obj writeAPIError(w, r, err) return } - defer body.Close() + defer func() { + if err := body.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "serveObject", "error", err) + } + }() w.WriteHeader(206) - _, _ = io.CopyN(w, body, length) + if _, err := io.CopyN(w, body, length); err != nil { + slog.Error("传输响应失败", "module", "designcatalog", "operation", "serveObject", "error", err) + } return } bodies := []io.ReadCloser{} defer func() { for _, body := range bodies { - body.Close() + if err := body.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "serveObject", "error", err) + } } }() if r.Method != "HEAD" { @@ -184,13 +208,17 @@ func (s *Service) serveObject(w http.ResponseWriter, r *http.Request, object Obj header := textproto.MIMEHeader{"Content-Type": {contentType}, "Content-Range": {fmt.Sprintf("bytes %d-%d/%d", part.start, part.end, object.Size)}} dest, err := multi.CreatePart(header) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "designcatalog", "operation", "serveObject", "error", err) return } if _, err = io.CopyN(dest, bodies[i], part.end-part.start+1); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "designcatalog", "operation", "serveObject", "error", err) return } } - _ = multi.Close() + if err := multi.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "serveObject", "error", err) + } } func (s *Service) downloadAPI(w http.ResponseWriter, r *http.Request) { snap, q, err := s.selected(r) @@ -258,6 +286,9 @@ func (s *Service) mediaAPI(w http.ResponseWriter, r *http.Request) { } typ, _, parseErr := mime.ParseMediaType(ref.MIME) if parseErr != nil || !strings.HasPrefix(typ, "image/") && !strings.HasPrefix(typ, "video/") && !strings.HasPrefix(typ, "audio/") { + if parseErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "designcatalog", "operation", "mediaAPI", "error", parseErr) + } break } s.serveObject(w, r, Object{Key: ObjectKey(ref.SHA256), SHA256: ref.SHA256, Size: ref.Size}, ref.MIME, "") diff --git a/monkeyai/backend/internal/designcatalog/inspect.go b/monkeyai/backend/internal/designcatalog/inspect.go index 4a0a9e529..4f18a0f30 100644 --- a/monkeyai/backend/internal/designcatalog/inspect.go +++ b/monkeyai/backend/internal/designcatalog/inspect.go @@ -4,6 +4,7 @@ import ( "archive/zip" "encoding/json" "io" + "log/slog" "os" "strings" ) @@ -51,7 +52,11 @@ func Inspect(reader io.ReaderAt, size int64) (Inspection, error) { if err != nil { return out, err } - defer r.Close() + defer func() { + if err := r.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "Inspect", "error", err) + } + }() b, err := io.ReadAll(io.LimitReader(r, int64(f.UncompressedSize64)+1)) if err != nil { return out, err @@ -75,7 +80,9 @@ func Inspect(reader io.ReaderAt, size int64) (Inspection, error) { return out, nil } _, out.NewDesign = design["schema_version"] - _ = json.Unmarshal(design["schema_version"], &out.Design.SchemaVersion) + if err := json.Unmarshal(design["schema_version"], &out.Design.SchemaVersion); err != nil { + slog.Error("解析数据失败", "module", "designcatalog", "operation", "Inspect", "error", err) + } out.Design.Supported = out.Design.SchemaVersion == SchemaVersion && stringValue(design["digest_schema"]) == DigestSchema return out, nil } diff --git a/monkeyai/backend/internal/designcatalog/package.go b/monkeyai/backend/internal/designcatalog/package.go index 6a40a76eb..24003c757 100644 --- a/monkeyai/backend/internal/designcatalog/package.go +++ b/monkeyai/backend/internal/designcatalog/package.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io" + "log/slog" "os" "path" "strings" @@ -78,7 +79,11 @@ func (r *parser) copy(dst io.Writer, f *zip.File) (int64, error) { if err != nil { return 0, err } - defer src.Close() + defer func() { + if err := src.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "copy", "error", err) + } + }() remaining := MaxExpanded - r.bytes if remaining < 0 { return 0, errors.New("累计展开大小超限") @@ -130,7 +135,11 @@ func (r *parser) capture(f *zip.File) (string, int64, error) { if err != nil { return "", 0, err } - defer file.Close() + defer func() { + if err := file.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "capture", "error", err) + } + }() h := sha256.New() size, err := r.copy(io.MultiWriter(file, h), f) if err != nil { @@ -153,7 +162,11 @@ func ParseFile(ctx context.Context, name string) (*Package, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { + if err := f.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "ParseFile", "error", err) + } + }() stat, err := f.Stat() if err != nil { return nil, err @@ -178,7 +191,9 @@ func Parse(ctx context.Context, reader io.ReaderAt, size int64) (_ *Package, err p := &Package{dir: dir, objects: map[string]string{}} defer func() { if err != nil { - _ = p.Close() + if err := p.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "Parse", "error", err) + } } }() r := &parser{ctx: ctx, p: p} @@ -275,8 +290,14 @@ func Parse(ctx context.Context, reader io.ReaderAt, size int64) (_ *Package, err if err = decode(raw, &taxonomy); err != nil { return nil, err } - a, _ := canonicalHash(taxonomy) - b, _ := canonicalHash(p.Taxonomy) + a, err := canonicalHash(taxonomy) + if err != nil { + return nil, err + } + b, err := canonicalHash(p.Taxonomy) + if err != nil { + return nil, err + } if a != b { return nil, errors.New("taxonomy 不一致") } @@ -357,8 +378,16 @@ func (r *parser) nested(outer *archive, e *Entry) error { if err != nil { return err } - defer os.Remove(file.Name()) - defer file.Close() + defer func() { + if err := os.Remove(file.Name()); err != nil && !os.IsNotExist(err) { + slog.Error("清理临时文件失败", "module", "designcatalog", "operation", "nested", "error", err) + } + }() + defer func() { + if err := file.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "nested", "error", err) + } + }() if _, err = r.copy(file, outer.files[e.Artifact.Path]); err != nil { return err } @@ -420,8 +449,14 @@ func (r *parser) nested(outer *archive, e *Entry) error { refs[ref.Source] = ref.Archive } if mapping != nil { - a, _ := canonicalHash(mapping) - b, _ := canonicalHash(refs) + a, err := canonicalHash(mapping) + if err != nil { + return err + } + b, err := canonicalHash(refs) + if err != nil { + return err + } if a != b { return errors.New("两种源路径映射不一致") } @@ -471,9 +506,13 @@ func (r *parser) nested(outer *archive, e *Entry) error { } } var fields map[string]json.RawMessage - _ = json.Unmarshal(e.Metadata, &fields) + if err := json.Unmarshal(e.Metadata, &fields); err != nil { + slog.Error("解析数据失败", "module", "designcatalog", "operation", "nested", "error", err) + } var export map[string]json.RawMessage - _ = json.Unmarshal(raw, &export) + if err := json.Unmarshal(raw, &export); err != nil { + slog.Error("解析数据失败", "module", "designcatalog", "operation", "nested", "error", err) + } if e.Kind == "design-system" { body, err := r.read(nz, meta.Body) if err != nil { @@ -504,9 +543,15 @@ func (r *parser) nested(outer *archive, e *Entry) error { } texts[lang] = string(text) } - a, _ := canonicalHash(texts) + a, err := canonicalHash(texts) + if err != nil { + return err + } want, err := Canonical(fields[pair[1]]) if err != nil || a != Hash(want) { + if err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "nested", "error", err) + } return errors.New("外置提示词不符") } } else { diff --git a/monkeyai/backend/internal/designcatalog/postgres.go b/monkeyai/backend/internal/designcatalog/postgres.go index 552b8b4ff..221107063 100644 --- a/monkeyai/backend/internal/designcatalog/postgres.go +++ b/monkeyai/backend/internal/designcatalog/postgres.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "slices" "strings" "uuid" @@ -59,7 +60,13 @@ type Snapshot struct { Taxonomy Taxonomy } -func payload(v any) []byte { b, _ := json.Marshal(v); return b } +func payload(v any) []byte { + b, err := json.Marshal(v) + if err != nil { + slog.Error("编码设计目录数据失败", "error", err) + } + return b +} func (s *Service) Current(ctx context.Context) (*SnapshotRef, int64, error) { row, err := sqlc.New(s.pool).Current(ctx) if err != nil { @@ -92,12 +99,18 @@ func snapshot(ctx context.Context, q *sqlc.Queries, publisher, release string) ( return nil, err } if digest, checkErr := ContentDigest(e); checkErr != nil || digest != e.ContentDigest { + if checkErr != nil { + slog.WarnContext(ctx, "操作失败", "module", "designcatalog", "operation", "snapshot", "error", checkErr) + } return nil, fault(503, "unavailable", "快照资源元数据损坏") } out.Entries = append(out.Entries, e) } computed, checkErr := (&Package{SnapshotRef: out.SnapshotRef, Entries: out.Entries, Taxonomy: out.Taxonomy}).Digest() if checkErr != nil || computed != out.Digest { + if checkErr != nil { + slog.WarnContext(ctx, "操作失败", "module", "designcatalog", "operation", "snapshot", "error", checkErr) + } return nil, fault(503, "unavailable", "快照指纹损坏") } return out, nil @@ -154,7 +167,9 @@ func (s *Service) plan(ctx context.Context, q *sqlc.Queries, p *Package) (Plan, } } var fields map[string]json.RawMessage - _ = json.Unmarshal(e.Metadata, &fields) + if err := json.Unmarshal(e.Metadata, &fields); err != nil { + slog.Error("解析数据失败", "module", "designcatalog", "operation", "plan", "error", err) + } name := stringValue(fields["name"]) if name == "" { name = e.ID @@ -196,7 +211,9 @@ func (s *Service) Apply(ctx context.Context, tx pgx.Tx, p *Package, actor, expec } for _, entry := range p.Entries { var fields map[string]json.RawMessage - _ = json.Unmarshal(entry.Metadata, &fields) + if err := json.Unmarshal(entry.Metadata, &fields); err != nil { + slog.Error("解析数据失败", "module", "designcatalog", "operation", "Apply", "error", err) + } if (entry.Kind == "design-system" || entry.Kind == "template" && boolValue(fields["downloadable"])) && entry.Native == nil { return plan, fmt.Errorf("资源 ZIP 尚未持久化: %s", entry.ID) } diff --git a/monkeyai/backend/internal/designcatalog/query.go b/monkeyai/backend/internal/designcatalog/query.go index aac2a9cb0..4eba3c712 100644 --- a/monkeyai/backend/internal/designcatalog/query.go +++ b/monkeyai/backend/internal/designcatalog/query.go @@ -2,6 +2,7 @@ package designcatalog import ( "encoding/json" + "log/slog" "net/http" "net/url" "slices" @@ -77,7 +78,9 @@ func boundPreview(value any, base string) bool { func (s *Service) present(snap *Snapshot, e Entry, detail bool) map[string]any { var metadata map[string]any - _ = json.Unmarshal(e.Metadata, &metadata) + if err := json.Unmarshal(e.Metadata, &metadata); err != nil { + slog.Error("解析数据失败", "module", "designcatalog", "operation", "present", "error", err) + } out := map[string]any{"id": e.ID, "content_digest": e.ContentDigest, "asset_snapshot_id": "release:" + snap.ReleaseID} fields := map[string][]string{ "template": {"template_kind", "downloadable", "has_skill", "name", "localizedName", "description", "localizedDescription", "mode", "category"}, @@ -192,6 +195,9 @@ func pageValue(q url.Values, key string, defaultValue int) (int, error) { } n, err := strconv.ParseInt(raw[0], 10, 64) if err != nil || n < 1 { + if err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "pageValue", "error", err) + } return 0, fault(400, "invalid_argument", "分页参数无效") } return int(n), nil @@ -201,7 +207,9 @@ func termMatches(actual, requested string) bool { } func matches(e Entry, q url.Values, t Taxonomy) bool { var meta map[string]json.RawMessage - _ = json.Unmarshal(e.Metadata, &meta) + if err := json.Unmarshal(e.Metadata, &meta); err != nil { + slog.Error("解析数据失败", "module", "designcatalog", "operation", "matches", "error", err) + } if text := strings.ToLower(q.Get("q")); text != "" && !strings.Contains(strings.ToLower(e.ID+" "+string(e.Metadata)), text) { return false } diff --git a/monkeyai/backend/internal/designcatalog/storage.go b/monkeyai/backend/internal/designcatalog/storage.go index 61d4dccc1..c1583ae82 100644 --- a/monkeyai/backend/internal/designcatalog/storage.go +++ b/monkeyai/backend/internal/designcatalog/storage.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "fmt" "io" + "log/slog" "os" "slices" "strings" @@ -33,7 +34,9 @@ func (s *Service) Prepare(ctx context.Context, p *Package) error { return err } err = s.storage.PutImmutable(ctx, ObjectKey(ref.SHA256), f, ref.Size, ref.MIME, ref.SHA256) - f.Close() + if err := f.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "Prepare", "error", err) + } if err != nil { return err } @@ -55,7 +58,9 @@ func (s *Service) Prepare(ctx context.Context, p *Package) error { return err } err = s.storage.PutImmutable(ctx, ObjectKey(native.SHA256), f, native.Size, "application/zip", native.SHA256) - f.Close() + if err := f.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "Prepare", "error", err) + } if err != nil { return err } @@ -69,10 +74,16 @@ func (p *Package) native(ctx context.Context, e *Entry) (_ *Object, err error) { if err != nil { return nil, err } - defer f.Close() + defer func() { + if err := f.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "native", "error", err) + } + }() defer func() { if err != nil { - os.Remove(f.Name()) + if err := os.Remove(f.Name()); err != nil && !os.IsNotExist(err) { + slog.Error("清理临时文件失败", "module", "designcatalog", "operation", "native", "error", err) + } } }() h := sha256.New() @@ -96,7 +107,9 @@ func (p *Package) native(ctx context.Context, e *Entry) (_ *Object, err error) { return nil, openErr } n, copyErr := io.Copy(out, src) - src.Close() + if err := src.Close(); err != nil { + slog.Error("关闭资源失败", "module", "designcatalog", "operation", "native", "error", err) + } if copyErr != nil { return nil, copyErr } diff --git a/monkeyai/backend/internal/designcatalog/tokens.go b/monkeyai/backend/internal/designcatalog/tokens.go index c257d2d62..aee15466b 100644 --- a/monkeyai/backend/internal/designcatalog/tokens.go +++ b/monkeyai/backend/internal/designcatalog/tokens.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/base64" "encoding/json" + "log/slog" "strings" "time" ) @@ -42,6 +43,7 @@ func (s *Service) mediaClaims(token string, now time.Time) (Claims, error) { } header, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "mediaClaims", "error", err) return c, unauthorized } var h struct { @@ -54,6 +56,7 @@ func (s *Service) mediaClaims(token string, now time.Time) (Claims, error) { } signature, err := base64.RawURLEncoding.DecodeString(parts[2]) if err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "mediaClaims", "error", err) return c, unauthorized } mac := hmac.New(sha256.New, []byte(s.access.MediaSecret)) @@ -63,6 +66,9 @@ func (s *Service) mediaClaims(token string, now time.Time) (Claims, error) { } body, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil || decode(body, &c) != nil { + if err != nil { + slog.Warn("操作失败", "module", "designcatalog", "operation", "mediaClaims", "error", err) + } return c, unauthorized } n := now.Unix() diff --git a/monkeyai/backend/internal/endpoint/connection.go b/monkeyai/backend/internal/endpoint/connection.go index a2151d62a..1210ed241 100644 --- a/monkeyai/backend/internal/endpoint/connection.go +++ b/monkeyai/backend/internal/endpoint/connection.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net" @@ -15,6 +14,8 @@ import ( "time" "unicode/utf8" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/coder/websocket" ) @@ -157,7 +158,7 @@ func (c *connection) writer() { code = 1000 } if err := c.ws.Close(websocket.StatusCode(code), ""); err != nil && !c.expected(err) { - logging.Event(context.Background(), c.service.logger, slog.LevelWarn, "endpoint.operation.warn", "端点 WebSocket 关闭失败", "module", "endpoint", "operation", "writer", "user_id", c.credential.UserID, "machine_id", c.machine, "operation", "ws_close", "close_code", code, "error_type", fmt.Sprintf("%T", err)) + logging.Event(context.Background(), c.service.logger, slog.LevelWarn, "endpoint.operation.warn", "端点 WebSocket 关闭失败", "module", "endpoint", "operation", "writer", "user_id", c.credential.UserID, "machine_id", c.machine, "operation", "ws_close", "close_code", code, "error_type", fmt.Sprintf("%T", err), "error", err) } c.cancel() }() @@ -244,6 +245,7 @@ func (c *connection) reader() { if err := json.Unmarshal(data, &envelope); err == nil { if raw, ok := envelope["type"]; ok { if err := json.Unmarshal(raw, &messageType); err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "reader", "error", err) messageType = "" } } @@ -254,6 +256,7 @@ func (c *connection) reader() { } m, err := message(data) if err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "reader", "error", err) c.reject("invalid_message", "") violations++ if violations >= 3 { diff --git a/monkeyai/backend/internal/endpoint/execution.go b/monkeyai/backend/internal/endpoint/execution.go index a6a0ccea4..c9a633a0b 100644 --- a/monkeyai/backend/internal/endpoint/execution.go +++ b/monkeyai/backend/internal/endpoint/execution.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "log/slog" "net/http" "slices" "time" @@ -75,7 +76,9 @@ func executionState(row sqlc.Endpoint) ExecutionState { PublicKey: slices.Clone(row.ExecutionPublicKey), RemoteAccessState: row.RemoteAccessState, RegistrationVersion: row.RegistrationVersion, GrantVersion: row.GrantVersion, LeaseEpoch: row.LeaseEpoch, LastSeenAt: row.LastSeenAt, RuntimeVersion: row.RuntimeVersion, EngineVersion: row.EngineVersion} - json.Unmarshal(row.ExecutionProtocols, &out.Protocols) + if err := json.Unmarshal(row.ExecutionProtocols, &out.Protocols); err != nil { + slog.Error("解析数据失败", "module", "endpoint", "operation", "executionState", "error", err) + } if row.Alias != nil { out.DisplayName = *row.Alias } @@ -233,7 +236,10 @@ func (e *Execution) Enroll(ctx context.Context, p identity.Principal, in Enrollm revokedGrant = *old.ActiveGrantID } } - protocols, _ := json.Marshal(in.Protocols) + protocols, err := json.Marshal(in.Protocols) + if err != nil { + return ExecutionState{}, err + } if in.Protocols == nil { protocols = []byte("{}") } diff --git a/monkeyai/backend/internal/endpoint/handler.go b/monkeyai/backend/internal/endpoint/handler.go index 0c7bec1e9..9f08244d9 100644 --- a/monkeyai/backend/internal/endpoint/handler.go +++ b/monkeyai/backend/internal/endpoint/handler.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net" @@ -17,6 +16,8 @@ import ( "time" "unicode/utf8" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/coder/websocket" "github.com/go-chi/chi/v5" @@ -161,6 +162,7 @@ func (s *Service) update(w http.ResponseWriter, r *http.Request, action string) } data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1024)) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "endpoint", "operation", "update", "error", err) failure(r.Context(), w, 400, "invalid_request") return } @@ -169,6 +171,9 @@ func (s *Service) update(w http.ResponseWriter, r *http.Request, action string) fields, err := object(data) raw, ok := fields["alias"] if err != nil || !ok || len(fields) != 1 || json.Unmarshal(raw, &alias) != nil || (alias != nil && !text(*alias, 128)) { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "endpoint", "operation", "update", "error", err) + } failure(r.Context(), w, 400, "invalid_request") return } @@ -197,6 +202,9 @@ func (s *Service) update(w http.ResponseWriter, r *http.Request, action string) func origin(value string) string { u, err := url.Parse(value) if err != nil || u.User != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + if err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "origin", "error", err) + } return "" } host := strings.ToLower(u.Hostname()) @@ -218,6 +226,9 @@ func (s *Service) connect(w http.ResponseWriter, r *http.Request) { o := values[0] parsed, err := url.Parse(o) if len(values) != 1 || err != nil || parsed.Path != "" || parsed.RawQuery != "" || parsed.Fragment != "" || origin(o) == "" || origin(o) != origin(s.origin) { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "endpoint", "operation", "connect", "error", err) + } failure(r.Context(), w, 403, "forbidden") return } @@ -228,6 +239,7 @@ func (s *Service) connect(w http.ResponseWriter, r *http.Request) { err := u.enter(ctx) cancel() if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "endpoint", "operation", "connect", "error", err) failure(r.Context(), w, 503, "service_unavailable") return } @@ -262,7 +274,9 @@ func (s *Service) connect(w http.ResponseWriter, r *http.Request) { if s.draining.Load() { s.mu.Unlock() cancel() - ws.CloseNow() + if err := ws.CloseNow(); err != nil { + slog.Error("关闭资源失败", "module", "endpoint", "operation", "connect", "error", err) + } return } s.connections[c] = struct{}{} @@ -273,7 +287,9 @@ func (s *Service) connect(w http.ResponseWriter, r *http.Request) { c.stop(1000) workers.Wait() cancel() - ws.CloseNow() + if err := ws.CloseNow(); err != nil { + slog.Error("关闭资源失败", "module", "endpoint", "operation", "connect", "error", err) + } cleanup, cancel := context.WithTimeout(context.Background(), s.timeDB) defer cancel() // 清理必须完成,不能因管理操作持锁而遗留一个永久离线连接。 diff --git a/monkeyai/backend/internal/endpoint/postgres.go b/monkeyai/backend/internal/endpoint/postgres.go index ac7d2d93d..ca44ff18d 100644 --- a/monkeyai/backend/internal/endpoint/postgres.go +++ b/monkeyai/backend/internal/endpoint/postgres.go @@ -4,10 +4,11 @@ import ( "context" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/audit" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/endpoint/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" @@ -113,7 +114,7 @@ func rollback(ctx context.Context, tx pgx.Tx, user, operation string) { if err := tx.Rollback(ctx); err != nil && ctx.Err() == nil && !errors.Is(err, pgx.ErrTxClosed) && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "endpoint.operation.warn", "端点事务回滚失败", "module", "endpoint", "operation", "rollback", "user_id", user, "operation", operation, "error_type", fmt.Sprintf("%T", err)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "endpoint.operation.warn", "端点事务回滚失败", "module", "endpoint", "operation", "rollback", "user_id", user, "operation", operation, "error_type", fmt.Sprintf("%T", err), "error", err) } } diff --git a/monkeyai/backend/internal/endpoint/protocol.go b/monkeyai/backend/internal/endpoint/protocol.go index f16a234e3..f5bf4331b 100644 --- a/monkeyai/backend/internal/endpoint/protocol.go +++ b/monkeyai/backend/internal/endpoint/protocol.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "io" + "log/slog" "regexp" "slices" "strings" @@ -72,6 +73,7 @@ func object(data []byte) (map[string]json.RawMessage, error) { decoder := json.NewDecoder(bytes.NewReader(data)) decoder.UseNumber() if err := value(decoder, 0); err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "object", "error", err) return nil, errInvalid } if _, err := decoder.Token(); err != io.EOF { @@ -124,6 +126,9 @@ func value(d *json.Decoder, depth int) error { } end, err := d.Token() if err != nil || (delim == '{' && end != json.Delim('}')) || (delim == '[' && end != json.Delim(']')) { + if err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "value", "error", err) + } return errInvalid } return nil @@ -157,6 +162,9 @@ func hello(data []byte) (Hello, error) { var h Hello fields, err := object(data) if err != nil || len(data) > 4096 || reserved(fields) || decode(fields, &h, "type", "protocol_versions", "machine_id") != nil { + if err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "hello", "error", err) + } return h, errInvalid } for _, key := range []string{"message_id", "target", "method", "reply_to", "payload"} { @@ -176,6 +184,9 @@ func hello(data []byte) (Hello, error) { } profile, err := object(fields["profile"]) if err != nil || decode(profile, &h.Profile, "device_name", "platform", "os_version", "arch", "client_version") != nil { + if err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "hello", "error", err) + } return h, errInvalid } p := h.Profile @@ -192,6 +203,9 @@ func message(data []byte) (Message, error) { var m Message fields, err := object(data) if err != nil || reserved(fields) || decode(fields, &m, "type", "message_id", "target", "method", "reply_to", "payload") != nil { + if err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "message", "error", err) + } return m, errInvalid } for _, key := range []string{"protocol_versions", "machine_id", "profile"} { @@ -203,6 +217,7 @@ func message(data []byte) (Message, error) { return m, errInvalid } if _, err := object(m.Payload); err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "message", "error", err) return m, errInvalid } switch m.Type { diff --git a/monkeyai/backend/internal/endpoint/protocol_test.go b/monkeyai/backend/internal/endpoint/protocol_test.go index 1118fe127..2aeda0ba4 100644 --- a/monkeyai/backend/internal/endpoint/protocol_test.go +++ b/monkeyai/backend/internal/endpoint/protocol_test.go @@ -12,8 +12,9 @@ import ( "strings" "syscall" - "github.com/jackc/pgx/v5" "testing" + + "github.com/jackc/pgx/v5" ) const machineA = "4f1207be-1ce0-4e88-8e3e-e92690567ec8" @@ -121,7 +122,7 @@ type rollbackTestTx struct { func (tx rollbackTestTx) Rollback(context.Context) error { return tx.err } -func TestRollbackLoggingSkipsNormalErrorsAndRedactsDetails(t *testing.T) { +func TestRollbackLoggingSkipsNormalErrorsAndKeepsDetails(t *testing.T) { var logs bytes.Buffer previous := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(&logs, nil))) @@ -137,7 +138,7 @@ func TestRollbackLoggingSkipsNormalErrorsAndRedactsDetails(t *testing.T) { } rollback(context.Background(), rollbackTestTx{err: errors.New("https://example.com/?token=private-token")}, "user-1", "page") if !strings.Contains(logs.String(), "error_type=") || !strings.Contains(logs.String(), "user_id=user-1") || - strings.Contains(logs.String(), "private-token") || strings.Contains(logs.String(), "token=") { - t.Fatalf("回滚失败日志缺少安全上下文或泄露敏感信息: %s", logs.String()) + !strings.Contains(logs.String(), "https://example.com/?token=private-token") { + t.Fatalf("回滚失败日志缺少上下文或原始错误: %s", logs.String()) } } diff --git a/monkeyai/backend/internal/endpoint/register.go b/monkeyai/backend/internal/endpoint/register.go index 39605e02a..f66f0b0d4 100644 --- a/monkeyai/backend/internal/endpoint/register.go +++ b/monkeyai/backend/internal/endpoint/register.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "io" + "log/slog" "net/http" "slices" @@ -36,6 +37,7 @@ func registration(data []byte) (Registration, error) { var r Registration fields, err := object(data) if err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "registration", "error", err) return r, errInvalid } allowed := []string{ @@ -49,6 +51,7 @@ func registration(data []byte) (Registration, error) { } } if err := json.Unmarshal(data, &r); err != nil { + slog.Warn("操作失败", "module", "endpoint", "operation", "registration", "error", err) return r, errInvalid } if !text(r.DeviceName, 128) || !text(r.OSVersion, 64) || !text(r.Arch, 32) || !text(r.ClientVersion, 64) || @@ -79,11 +82,13 @@ func (s *Service) register(w http.ResponseWriter, req *http.Request) { } data, err := io.ReadAll(http.MaxBytesReader(w, req.Body, 4096)) if err != nil { + slog.WarnContext(req.Context(), "操作失败", "module", "endpoint", "operation", "register", "error", err) failure(req.Context(), w, 400, "invalid_request") return } r, err := registration(data) if err != nil { + slog.WarnContext(req.Context(), "操作失败", "module", "endpoint", "operation", "register", "error", err) failure(req.Context(), w, 400, "invalid_request") return } diff --git a/monkeyai/backend/internal/endpoint/service.go b/monkeyai/backend/internal/endpoint/service.go index 58ca8b0d0..6f3faeb21 100644 --- a/monkeyai/backend/internal/endpoint/service.go +++ b/monkeyai/backend/internal/endpoint/service.go @@ -4,14 +4,15 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "sort" "sync" "sync/atomic" "time" + + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" ) type Credential struct { @@ -255,7 +256,9 @@ func (s *Service) Shutdown(ctx context.Context) error { case <-c.finished: case <-ctx.Done(): for _, pending := range all { - pending.ws.CloseNow() + if err := pending.ws.CloseNow(); err != nil { + slog.Error("关闭资源失败", "module", "endpoint", "operation", "Shutdown", "error", err) + } pending.cancel() } return ctx.Err() diff --git a/monkeyai/backend/internal/expert/avatar.go b/monkeyai/backend/internal/expert/avatar.go index 7df7e69ac..c0ade9e9d 100644 --- a/monkeyai/backend/internal/expert/avatar.go +++ b/monkeyai/backend/internal/expert/avatar.go @@ -2,6 +2,7 @@ package expert import ( "io" + "log/slog" "net/http" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/expert/sqlc" @@ -44,13 +45,22 @@ func (s *Service) serveAvatar(w http.ResponseWriter, req *http.Request, admin bo resource.Fail(req.Context(), w, err) return } - defer body.Close() + defer func() { + if err := body.Close(); err != nil { + slog.Error("关闭资源失败", "module", "expert", "operation", "serveAvatar", "error", err) + } + }() data, err := io.ReadAll(io.LimitReader(body, 2<<20)) if err != nil || len(data) >= 2<<20 { + if err != nil { + slog.WarnContext(req.Context(), "操作失败", "module", "expert", "operation", "serveAvatar", "error", err) + } resource.Fail(req.Context(), w, &resource.Error{Status: 502, Code: "storage_corrupted", Message: "专家头像读取失败"}) return } w.Header().Set("Content-Type", "image/webp") w.Header().Set("Cache-Control", "private, max-age=3600") - w.Write(data) + if _, err := w.Write(data); err != nil { + slog.Error("写入输出失败", "module", "expert", "operation", "serveAvatar", "error", err) + } } diff --git a/monkeyai/backend/internal/expert/service.go b/monkeyai/backend/internal/expert/service.go index 0f65bfae8..da0f2ff2e 100644 --- a/monkeyai/backend/internal/expert/service.go +++ b/monkeyai/backend/internal/expert/service.go @@ -4,11 +4,12 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "strings" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/expert/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" @@ -46,6 +47,9 @@ func (s *Service) validate(ctx context.Context, tx pgx.Tx, in, old resource.Obje Deny []string `json:"tool_denylist"` } if err != nil || string(b) == "null" || json.Unmarshal(b, &links) != nil { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "expert", "operation", "validate", "error", err) + } return resource.Invalid("连接依赖格式无效") } for _, link := range links { diff --git a/monkeyai/backend/internal/feedback/admin.go b/monkeyai/backend/internal/feedback/admin.go index c13729fb5..dfffe221d 100644 --- a/monkeyai/backend/internal/feedback/admin.go +++ b/monkeyai/backend/internal/feedback/admin.go @@ -2,7 +2,6 @@ package feedback import ( "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net/http" @@ -10,6 +9,8 @@ import ( "strconv" "strings" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/feedback/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" "github.com/go-chi/chi/v5" @@ -152,6 +153,7 @@ func (s *Service) download(w http.ResponseWriter, r *http.Request) { } body, err := s.storage.Get(r.Context(), row.ObjectKey) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "feedback", "operation", "download", "error", err) resource.Fail(r.Context(), w, storageFailure()) return } diff --git a/monkeyai/backend/internal/feedback/agent.go b/monkeyai/backend/internal/feedback/agent.go index 755f34788..3bc69b267 100644 --- a/monkeyai/backend/internal/feedback/agent.go +++ b/monkeyai/backend/internal/feedback/agent.go @@ -2,7 +2,6 @@ package feedback import ( "bytes" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "image" _ "image/jpeg" _ "image/png" @@ -14,6 +13,8 @@ import ( "strings" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" "github.com/go-chi/chi/v5" @@ -61,6 +62,7 @@ func (s *Service) create(w http.ResponseWriter, r *http.Request) { }() } if parseErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "feedback", "operation", "create", "error", parseErr) resource.Fail(r.Context(), w, resource.Invalid("反馈请求无效或超过 16 MiB")) return } @@ -114,6 +116,9 @@ func parseRating(value string) (*int32, error) { } n, err := strconv.ParseInt(value, 10, 32) if err != nil || n < 1 || n > 5 { + if err != nil { + slog.Warn("操作失败", "module", "feedback", "operation", "parseRating", "error", err) + } return nil, resource.Invalid("评分必须为 1 至 5") } rating := int32(n) @@ -126,11 +131,19 @@ func readImage(header *multipart.FileHeader) (Image, error) { } file, err := header.Open() if err != nil { + slog.Warn("操作失败", "module", "feedback", "operation", "readImage", "error", err) return Image{}, resource.Invalid("读取图片失败") } - defer func() { _ = file.Close() }() + defer func() { + if err := file.Close(); err != nil { + slog.Error("关闭资源失败", "module", "feedback", "operation", "readImage", "error", err) + } + }() data, err := io.ReadAll(io.LimitReader(file, maxImageBytes+1)) if err != nil || len(data) == 0 || len(data) > maxImageBytes { + if err != nil { + slog.Warn("操作失败", "module", "feedback", "operation", "readImage", "error", err) + } return Image{}, resource.Invalid("单张图片不能超过 5 MiB") } return decodeImage(data) @@ -139,10 +152,16 @@ func readImage(header *multipart.FileHeader) (Image, error) { func decodeImage(data []byte) (Image, error) { config, _, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil || config.Width <= 0 || config.Height <= 0 || int64(config.Width)*int64(config.Height) > maxImagePixels { + if err != nil { + slog.Warn("操作失败", "module", "feedback", "operation", "decodeImage", "error", err) + } return Image{}, resource.Invalid("图片格式、尺寸或像素数无效") } decoded, format, err := image.Decode(bytes.NewReader(data)) if err != nil || decoded.Bounds().Dx() != config.Width || decoded.Bounds().Dy() != config.Height { + if err != nil { + slog.Warn("操作失败", "module", "feedback", "operation", "decodeImage", "error", err) + } return Image{}, resource.Invalid("图片内容无效") } if format != "png" && format != "jpeg" && format != "webp" { diff --git a/monkeyai/backend/internal/feedback/feedback.go b/monkeyai/backend/internal/feedback/feedback.go index 9e91f14de..189043211 100644 --- a/monkeyai/backend/internal/feedback/feedback.go +++ b/monkeyai/backend/internal/feedback/feedback.go @@ -7,13 +7,14 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "slices" "strings" "time" "unicode/utf8" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/feedback/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" "github.com/jackc/pgx/v5" @@ -151,6 +152,7 @@ func (s *Service) Submit(ctx context.Context, userID string, in Request) (Feedba attachmentIDs = append(attachmentIDs, attachmentID) objectKeys = append(objectKeys, key) if err := s.storage.Put(ctx, key, image.Data, image.MIMEType); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "feedback", "operation", "Submit", "error", err) return Feedback{}, true, s.fail(ctx, row.ID, attachmentIDs, objectKeys, storageFailure()) } count, err := sqlc.New(s.pool).SetAttachmentReady(ctx, sqlc.SetAttachmentReadyParams{ID: attachmentID, FeedbackID: row.ID}) @@ -210,7 +212,7 @@ func (s *Service) fail(ctx context.Context, feedbackID string, attachmentIDs []s defer cancel() for _, key := range objectKeys { if err := s.storage.Delete(cleanupCtx, key); err != nil { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "feedback.operation.warn", "补偿删除反馈附件失败", "module", "feedback", "operation", "fail", "feedback_id", feedbackID, "error_type", fmt.Sprintf("%T", err)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "feedback.operation.warn", "补偿删除反馈附件失败", "module", "feedback", "operation", "fail", "feedback_id", feedbackID, "error_type", fmt.Sprintf("%T", err), "error", err) } } return cause diff --git a/monkeyai/backend/internal/group/agent.go b/monkeyai/backend/internal/group/agent.go index 9c3237ca3..8651da24d 100644 --- a/monkeyai/backend/internal/group/agent.go +++ b/monkeyai/backend/internal/group/agent.go @@ -2,6 +2,7 @@ package group import ( "context" + "log/slog" "net/http" "strconv" "strings" @@ -27,6 +28,9 @@ func (s *Service) RegisterAgent(router chi.Router) { limit, err = strconv.Atoi(raw) } if err != nil || limit < 1 || limit > 100 { + if err != nil { + slog.Warn("操作失败", "module", "group", "operation", "RegisterAgent", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("limit 必须为 1—100")) return } diff --git a/monkeyai/backend/internal/group/service.go b/monkeyai/backend/internal/group/service.go index b3bdaba5a..8395eb163 100644 --- a/monkeyai/backend/internal/group/service.go +++ b/monkeyai/backend/internal/group/service.go @@ -4,12 +4,13 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "slices" "strings" "unicode/utf8" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/group/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/rootgroup" @@ -136,6 +137,7 @@ func (s *Service) Save(ctx context.Context, actor, id string, in Input) (Group, var parentID *string if len(in.ParentID) > 0 { if err := json.Unmarshal(in.ParentID, &parentID); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "group", "operation", "Save", "error", err) return Group{}, resource.Invalid("parent_id 必须是分组 ID 或 null") } if parentID != nil && *parentID == rootgroup.ID { diff --git a/monkeyai/backend/internal/httpapi/router_test.go b/monkeyai/backend/internal/httpapi/router_test.go index dcd6f4d43..a4b121307 100644 --- a/monkeyai/backend/internal/httpapi/router_test.go +++ b/monkeyai/backend/internal/httpapi/router_test.go @@ -63,7 +63,7 @@ func TestHealthWriteFailureLogged(t *testing.T) { var logs bytes.Buffer handler := New(slog.New(slog.NewTextHandler(&logs, nil)), stubPinger{}, http.NotFoundHandler(), http.NotFoundHandler(), http.NotFoundHandler()) handler.ServeHTTP(brokenWriter{httptest.NewRecorder()}, httptest.NewRequest(http.MethodGet, "/healthz", nil)) - if !strings.Contains(logs.String(), "event=http.response.failed") || !strings.Contains(logs.String(), "error_code=internal_error") || strings.Contains(logs.String(), "write failed") { + if !strings.Contains(logs.String(), "event=http.response.failed") || !strings.Contains(logs.String(), "error_code=internal_error") || !strings.Contains(logs.String(), "write failed") { t.Fatalf("缺少写入错误日志: %s", logs.String()) } } diff --git a/monkeyai/backend/internal/identity/admin.go b/monkeyai/backend/internal/identity/admin.go index 87bd327c3..b77fd400b 100644 --- a/monkeyai/backend/internal/identity/admin.go +++ b/monkeyai/backend/internal/identity/admin.go @@ -3,11 +3,12 @@ package identity import ( "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "strings" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/member" "github.com/go-chi/chi/v5" @@ -65,6 +66,7 @@ func (s *Service) patchUser(w http.ResponseWriter, r *http.Request) { Status string `json:"status"` } if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&input); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "patchUser", "error", err) writeError(r.Context(), w, http.StatusBadRequest, "invalid_request", "请求格式无效") return } @@ -172,6 +174,7 @@ func (s *Service) createUser(w http.ResponseWriter, r *http.Request) { GroupIDs []string `json:"group_ids"` } if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&input); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "createUser", "error", err) writeError(r.Context(), w, http.StatusBadRequest, "invalid_request", "请求格式无效") return } diff --git a/monkeyai/backend/internal/identity/admincreate.go b/monkeyai/backend/internal/identity/admincreate.go index 0ca62ab43..162331346 100644 --- a/monkeyai/backend/internal/identity/admincreate.go +++ b/monkeyai/backend/internal/identity/admincreate.go @@ -2,6 +2,7 @@ package identity import ( "errors" + "log/slog" "slices" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/rootgroup" @@ -21,6 +22,9 @@ func normalizeCreationGroups(ids []string) ([]string, error) { for _, id := range ids { var value pgtype.UUID if err := value.Scan(id); err != nil || !value.Valid { + if err != nil { + slog.Warn("操作失败", "module", "identity", "operation", "normalizeCreationGroups", "error", err) + } return nil, errors.New("group_ids 必须包含有效的分组 UUID") } if value.String() == rootgroup.ID { diff --git a/monkeyai/backend/internal/identity/callback.go b/monkeyai/backend/internal/identity/callback.go index 2397445bc..0924771e7 100644 --- a/monkeyai/backend/internal/identity/callback.go +++ b/monkeyai/backend/internal/identity/callback.go @@ -1,6 +1,7 @@ package identity import ( + "log/slog" "net" "net/url" "strconv" @@ -22,10 +23,16 @@ func validClientRedirect(clientID, raw string) bool { } u, err := url.Parse(raw) if err != nil || u.Scheme != "http" || u.Opaque != "" || u.User != nil || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" || u.EscapedPath() != "/oauth/callback" { + if err != nil { + slog.Warn("操作失败", "module", "identity", "operation", "validClientRedirect", "error", err) + } return false } host, port, err := net.SplitHostPort(u.Host) if err != nil || (host != "127.0.0.1" && host != "::1" && host != "localhost") { + if err != nil { + slog.Warn("操作失败", "module", "identity", "operation", "validClientRedirect", "error", err) + } return false } number, err := strconv.Atoi(port) diff --git a/monkeyai/backend/internal/identity/consent.go b/monkeyai/backend/internal/identity/consent.go index f1f0f49c8..a4e65c852 100644 --- a/monkeyai/backend/internal/identity/consent.go +++ b/monkeyai/backend/internal/identity/consent.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "io" + "log/slog" "net/http" "net/url" "strings" @@ -15,6 +16,9 @@ import ( func consentToken(r *http.Request, requestID string) string { cookie, err := r.Cookie(sessionCookie) if err != nil || cookie.Value == "" { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "consentToken", "error", err) + } return "" } mac := hmac.New(sha256.New, []byte(cookie.Value)) @@ -38,10 +42,16 @@ func verifyConsent(r *http.Request, requestID, publicURL string) error { func sameOrigin(origin, publicURL string) bool { actual, err := url.Parse(origin) if err != nil || actual.User != nil || actual.Path != "" || actual.RawQuery != "" || actual.Fragment != "" { + if err != nil { + slog.Warn("操作失败", "module", "identity", "operation", "sameOrigin", "error", err) + } return false } expected, err := url.Parse(publicURL) if err != nil || expected.Hostname() == "" { + if err != nil { + slog.Warn("操作失败", "module", "identity", "operation", "sameOrigin", "error", err) + } return false } return strings.EqualFold(actual.Scheme, expected.Scheme) && diff --git a/monkeyai/backend/internal/identity/device.go b/monkeyai/backend/internal/identity/device.go index c09af4bdd..f44451285 100644 --- a/monkeyai/backend/internal/identity/device.go +++ b/monkeyai/backend/internal/identity/device.go @@ -11,6 +11,7 @@ import ( "encoding/json" "errors" "io" + "log/slog" "net" "net/http" "net/url" @@ -61,6 +62,7 @@ func (s *Service) deviceLimit(r *http.Request, kind string, limit int) bool { } host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceLimit", "error", err) host = r.RemoteAddr } key := kind + ":" + tokenHash(host) @@ -165,6 +167,9 @@ func (s *Service) deviceAuthorization(w http.ResponseWriter, r *http.Request) { machine, display, platform, arch := r.PostForm.Get("machine_id"), r.PostForm.Get("display_name"), r.PostForm.Get("platform"), r.PostForm.Get("arch") public, err := base64.RawURLEncoding.Strict().DecodeString(r.PostForm.Get("execution_public_key")) if err != nil || len(public) != ed25519.PublicKeySize || !deviceUUID(machine) || !deviceText(display, 128) || !deviceText(arch, 32) || !slices.Contains([]string{"macos", "windows", "linux"}, platform) { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceAuthorization", "error", err) + } s.writeOAuthError(r.Context(), w, oauthError("invalid_request", "设备信息或公钥无效")) return } @@ -216,11 +221,13 @@ func (s *Service) deviceRequest(w http.ResponseWriter, r *http.Request) { return } if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceRequest", "error", err) writeError(r.Context(), w, 503, "authorization_state_unavailable", "设备申请暂不可用") return } connections, err := s.connections(r.Context()) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceRequest", "error", err) writeError(r.Context(), w, 503, "settings_unavailable", "登录方式暂不可用") return } @@ -229,14 +236,18 @@ func (s *Service) deviceRequest(w http.ResponseWriter, r *http.Request) { if authenticated { tx, beginErr := s.db.BeginTx(r.Context(), pgx.TxOptions{AccessMode: pgx.ReadOnly}) if beginErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceRequest", "error", beginErr) writeError(r.Context(), w, 503, "authorization_state_unavailable", "设备状态暂不可用") return } requiresRestore, err = s.device.registry.VerifyDeviceRequest(r.Context(), tx, user.ID, request.MachineID, request.ExecutionPublicKey) - tx.Rollback(r.Context()) + if err := tx.Rollback(r.Context()); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "identity", "operation", "deviceRequest", "error", err) + } if errors.Is(err, ErrDeviceBindingConflict) { conflict = true } else if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceRequest", "error", err) writeError(r.Context(), w, 503, "authorization_state_unavailable", "设备状态暂不可用") return } @@ -274,10 +285,15 @@ func (s *Service) deviceDecision(w http.ResponseWriter, r *http.Request) { user, _ := UserFromContext(r.Context()) tx, err := s.db.Begin(r.Context()) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceDecision", "error", err) writeError(r.Context(), w, 503, "authorization_state_unavailable", "授权状态暂不可用") return } - defer tx.Rollback(context.WithoutCancel(r.Context())) + defer func() { + if err := tx.Rollback(context.WithoutCancel(r.Context())); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "identity", "operation", "deviceDecision", "error", err) + } + }() q := sqlc.New(tx) request, err := q.LockDeviceRequest(r.Context(), in.RequestID) if errors.Is(err, pgx.ErrNoRows) { @@ -285,12 +301,17 @@ func (s *Service) deviceDecision(w http.ResponseWriter, r *http.Request) { return } if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceDecision", "error", err) writeError(r.Context(), w, 503, "authorization_state_unavailable", "设备申请暂不可用") return } if !s.now().Before(request.ExpiresAt) { - q.ResolveDeviceRequest(r.Context(), sqlc.ResolveDeviceRequestParams{ID: request.ID, Status: "expired"}) - tx.Commit(r.Context()) + if err := q.ResolveDeviceRequest(r.Context(), sqlc.ResolveDeviceRequestParams{ID: request.ID, Status: "expired"}); err != nil { + slog.Error("操作失败", "module", "identity", "operation", "deviceDecision", "error", err) + } + if err := tx.Commit(r.Context()); err != nil { + slog.Error("操作失败", "module", "identity", "operation", "deviceDecision", "error", err) + } writeError(r.Context(), w, 409, "device_request_resolved", "设备申请已过期") return } @@ -299,6 +320,7 @@ func (s *Service) deviceDecision(w http.ResponseWriter, r *http.Request) { return } if _, err = q.LockDeviceOwner(r.Context(), user.ID); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceDecision", "error", err) writeError(r.Context(), w, 403, "permission_denied", "账号不可用于设备授权") return } @@ -331,14 +353,19 @@ func (s *Service) deviceDecision(w http.ResponseWriter, r *http.Request) { } changed, err := q.DecideDeviceRequest(r.Context(), sqlc.DecideDeviceRequestParams{ID: request.ID, Status: status, OwnerUserID: new(user.ID), ApprovedScopes: scopes, RestoreRevoked: in.RestoreRevoked}) if err != nil || changed != 1 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceDecision", "error", err) + } writeError(r.Context(), w, 409, "device_request_resolved", "设备申请已被其他操作处理") return } if err = audit.Write(r.Context(), tx, audit.Event{ActorID: user.ID, Action: "device." + status, Category: "security", TargetType: "endpoint", TargetID: request.MachineID, Params: map[string]any{"request_id": request.ID, "scopes": scopes, "restore_revoked": in.RestoreRevoked}}); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceDecision", "error", err) writeError(r.Context(), w, 503, "authorization_state_unavailable", "无法保存授权决定") return } if err = tx.Commit(r.Context()); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "deviceDecision", "error", err) writeError(r.Context(), w, 503, "authorization_state_unavailable", "无法保存授权决定") return } @@ -353,7 +380,11 @@ func (s *Service) ExchangeDeviceCode(ctx context.Context, clientID, code string) if err != nil { return Token{}, err } - defer tx.Rollback(context.WithoutCancel(ctx)) + defer func() { + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "identity", "operation", "ExchangeDeviceCode", "error", err) + } + }() q := sqlc.New(tx) request, err := q.LockDeviceCode(ctx, sqlc.LockDeviceCodeParams{DeviceCodeHash: tokenHash(code), ClientID: clientID}) if errors.Is(err, pgx.ErrNoRows) { diff --git a/monkeyai/backend/internal/identity/email.go b/monkeyai/backend/internal/identity/email.go index 08bd34c4c..1f906d6a9 100644 --- a/monkeyai/backend/internal/identity/email.go +++ b/monkeyai/backend/internal/identity/email.go @@ -7,7 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "math/big" "net" @@ -15,6 +14,8 @@ import ( "strings" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/member" "github.com/jackc/pgx/v5" @@ -85,6 +86,9 @@ func readEmailInput(w http.ResponseWriter, r *http.Request) (emailInput, bool) { err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8192)).Decode(&input) input.Email = strings.ToLower(strings.TrimSpace(input.Email)) if err != nil || !validEmail(input.Email) || len(input.Email) > 254 || len(input.Password) > 1024 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "readEmailInput", "error", err) + } writeError(r.Context(), w, 400, "invalid_request", "请求格式或邮箱无效") return input, false } @@ -110,12 +114,14 @@ func (s *Service) sendCode(w http.ResponseWriter, r *http.Request) { } number, err := rand.Int(rand.Reader, big.NewInt(1_000_000)) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "sendCode", "error", err) writeError(r.Context(), w, 500, "server_error", "生成验证码失败") return } code := fmt.Sprintf("%06d", number.Int64()) ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "sendCode", "error", err) ip = r.RemoteAddr } err = s.reserveCode(r.Context(), input, code, tokenHash(ip)) diff --git a/monkeyai/backend/internal/identity/grant.go b/monkeyai/backend/internal/identity/grant.go index 48b47cf52..b45154434 100644 --- a/monkeyai/backend/internal/identity/grant.go +++ b/monkeyai/backend/internal/identity/grant.go @@ -3,6 +3,7 @@ package identity import ( "context" "errors" + "log/slog" "slices" "time" @@ -18,7 +19,11 @@ func (s *Service) redeemCodeAndStoreToken(ctx context.Context, code Authorizatio if err != nil { return sqlc.OauthGrant{}, err } - defer tx.Rollback(context.WithoutCancel(ctx)) + defer func() { + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "identity", "operation", "redeemCodeAndStoreToken", "error", err) + } + }() q := sqlc.New(tx) result, err := q.RedeemAuthorizationCode(ctx, code.ID) if err != nil { @@ -44,7 +49,11 @@ func (s *Service) rotateToken(ctx context.Context, oldRefreshHash, clientID, acc if err != nil { return sqlc.OauthGrant{}, err } - defer tx.Rollback(context.WithoutCancel(ctx)) + defer func() { + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "identity", "operation", "rotateToken", "error", err) + } + }() q := sqlc.New(tx) params := sqlc.GetRefreshTokenParams{RefreshTokenHash: oldRefreshHash, ClientID: clientID} old, err := q.GetRefreshToken(ctx, params) @@ -115,7 +124,11 @@ func (s *Service) RevokeGrant(ctx context.Context, id, reason string) error { if err != nil { return err } - defer tx.Rollback(context.WithoutCancel(ctx)) + defer func() { + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "identity", "operation", "RevokeGrant", "error", err) + } + }() q := sqlc.New(tx) if _, err := q.LockGrant(ctx, id); errors.Is(err, pgx.ErrNoRows) { return nil diff --git a/monkeyai/backend/internal/identity/middleware.go b/monkeyai/backend/internal/identity/middleware.go index 9b6354f55..d0e57bdee 100644 --- a/monkeyai/backend/internal/identity/middleware.go +++ b/monkeyai/backend/internal/identity/middleware.go @@ -24,6 +24,9 @@ func UserFromContext(ctx context.Context) (User, bool) { func (s *Service) BrowserUser(r *http.Request) (User, bool) { cookie, err := r.Cookie(sessionCookie) if err != nil || cookie.Value == "" { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "BrowserUser", "error", err) + } return User{}, false } user, _, err := s.userByBrowserToken(r.Context(), tokenHash(cookie.Value)) @@ -37,6 +40,9 @@ func (s *Service) RequireAdmin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(sessionCookie) if err != nil || cookie.Value == "" { + if err != nil { + slog.Warn("操作失败", "module", "identity", "operation", "RequireAdmin", "error", err) + } writeError(r.Context(), w, http.StatusUnauthorized, "unauthorized", "请先登录") return } diff --git a/monkeyai/backend/internal/identity/oauth.go b/monkeyai/backend/internal/identity/oauth.go index 145b60749..76f0545e8 100644 --- a/monkeyai/backend/internal/identity/oauth.go +++ b/monkeyai/backend/internal/identity/oauth.go @@ -3,12 +3,13 @@ package identity import ( "context" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "net/url" "slices" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/member" "github.com/go-chi/chi/v5" @@ -89,6 +90,7 @@ func (s *Service) OAuthMetadata(w http.ResponseWriter, r *http.Request) { } current, err := s.runtimeFeatures(r.Context()) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "OAuthMetadata", "error", err) writeError(r.Context(), w, 503, "capabilities_unavailable", "功能策略暂不可用") return } @@ -122,6 +124,7 @@ func (s *Service) authorize(w http.ResponseWriter, r *http.Request) { func (s *Service) token(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 64<<10) if err := r.ParseForm(); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "token", "error", err) s.writeOAuthError(r.Context(), w, oauthError("invalid_request", "请求格式无效")) return } @@ -153,6 +156,7 @@ func (s *Service) token(w http.ResponseWriter, r *http.Request) { func (s *Service) revoke(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 64<<10) if err := r.ParseForm(); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "revoke", "error", err) s.writeOAuthError(r.Context(), w, oauthError("invalid_request", "请求格式无效")) return } @@ -270,6 +274,7 @@ func (s *Service) completeClientRequest(w http.ResponseWriter, r *http.Request) requestID := chi.URLParam(r, "requestID") request, err := s.authorizationRequest(r.Context(), requestID) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "completeClientRequest", "error", err) writeError(r.Context(), w, http.StatusNotFound, "request_not_found", "授权请求不存在") return } @@ -299,6 +304,9 @@ func (s *Service) startUpstream(w http.ResponseWriter, r *http.Request) { } request, err := sqlc.New(s.db).DeviceRequestByID(r.Context(), requestID) if err != nil || request.Status != "pending" || !s.now().Before(request.ExpiresAt) { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "startUpstream", "error", err) + } writeError(r.Context(), w, 400, "request_unavailable", "设备申请已完成或过期") return } @@ -444,7 +452,7 @@ func (s *Service) clientLoginURL(requestID, errorCode string) string { func logUpstreamFailure(ctx context.Context, operation, connectionID string, err error) { if response, ok := errors.AsType[*upstreamHTTPError](err); ok { - logging.Event(ctx, slog.Default(), slog.LevelError, "identity.request.failed", "上游 OAuth 操作失败", "module", "identity", "operation", "logUpstreamFailure", "operation", operation, "connection_id", connectionID, "reason", "http_status", "status", response.status) + logging.Event(ctx, slog.Default(), slog.LevelError, "identity.request.failed", "上游 OAuth 操作失败", "module", "identity", "operation", "logUpstreamFailure", "operation", operation, "connection_id", connectionID, "reason", "http_status", "status", response.status, "error", err) return } reason := "invalid_upstream_response" @@ -461,5 +469,5 @@ func logUpstreamFailure(ctx context.Context, operation, connectionID string, err reason = "request_failed" } } - logging.Event(ctx, slog.Default(), slog.LevelError, "identity.request.failed", "上游 OAuth 操作失败", "module", "identity", "operation", "logUpstreamFailure", "operation", operation, "connection_id", connectionID, "reason", reason) + logging.Event(ctx, slog.Default(), slog.LevelError, "identity.request.failed", "上游 OAuth 操作失败", "module", "identity", "operation", "logUpstreamFailure", "operation", operation, "connection_id", connectionID, "reason", reason, "error", err) } diff --git a/monkeyai/backend/internal/identity/password.go b/monkeyai/backend/internal/identity/password.go index d6c1f3e9f..47cec3788 100644 --- a/monkeyai/backend/internal/identity/password.go +++ b/monkeyai/backend/internal/identity/password.go @@ -9,7 +9,6 @@ import ( "encoding/base64" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "math/big" "net/http" @@ -17,6 +16,8 @@ import ( "strconv" "strings" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/audit" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/member" @@ -73,6 +74,9 @@ func (s *Service) passwordLogin(w http.ResponseWriter, r *http.Request) { Password string `json:"password"` } if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&input); err != nil || len(input.Password) > 1024 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "passwordLogin", "error", err) + } writeError(r.Context(), w, http.StatusBadRequest, "invalid_request", "请求格式无效") return } @@ -95,6 +99,9 @@ func (s *Service) passwordLogin(w http.ResponseWriter, r *http.Request) { } valid := verifyPassword(input.Password, passwordHash) if err != nil || !valid { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "passwordLogin", "error", err) + } writeError(r.Context(), w, http.StatusUnauthorized, "invalid_credentials", "邮箱或密码错误") return } @@ -139,14 +146,23 @@ func verifyPassword(password, encoded string) bool { } iterations, err := strconv.Atoi(parts[2]) if err != nil || iterations < 100_000 || iterations > 2_000_000 { + if err != nil { + slog.Warn("操作失败", "module", "identity", "operation", "verifyPassword", "error", err) + } return false } salt, err := base64.RawStdEncoding.DecodeString(parts[3]) if err != nil || len(salt) < 16 { + if err != nil { + slog.Warn("操作失败", "module", "identity", "operation", "verifyPassword", "error", err) + } return false } expected, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil || len(expected) != 32 { + if err != nil { + slog.Warn("操作失败", "module", "identity", "operation", "verifyPassword", "error", err) + } return false } actual, err := pbkdf2.Key(sha256.New, password, salt, iterations, len(expected)) diff --git a/monkeyai/backend/internal/identity/reset.go b/monkeyai/backend/internal/identity/reset.go index fcf0458eb..481c49a87 100644 --- a/monkeyai/backend/internal/identity/reset.go +++ b/monkeyai/backend/internal/identity/reset.go @@ -2,10 +2,13 @@ package identity import ( "context" + "errors" "fmt" + "log/slog" "strings" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity/sqlc" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) @@ -23,7 +26,11 @@ func ResetAdminPassword(ctx context.Context, db *pgxpool.Pool, email string) (st if err != nil { return "", err } - defer tx.Rollback(ctx) + defer func() { + if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "identity", "operation", "ResetAdminPassword", "error", err) + } + }() q := sqlc.New(tx) user, err := q.ResetAdminPassword(ctx, sqlc.ResetAdminPasswordParams{ Email: strings.ToLower(strings.TrimSpace(email)), PasswordHash: new(hash), diff --git a/monkeyai/backend/internal/identity/search.go b/monkeyai/backend/internal/identity/search.go index a5cc60c25..8e121b6e9 100644 --- a/monkeyai/backend/internal/identity/search.go +++ b/monkeyai/backend/internal/identity/search.go @@ -2,12 +2,13 @@ package identity import ( "context" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "strconv" "strings" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity/sqlc" ) @@ -25,6 +26,9 @@ func (s *Service) searchUsers(w http.ResponseWriter, r *http.Request) { limit, err = strconv.Atoi(raw) } if err != nil || limit < 1 || limit > 100 || query == "" || len(query) > 200 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "identity", "operation", "searchUsers", "error", err) + } writeError(r.Context(), w, http.StatusBadRequest, "invalid_request", "q 必须为 1—200 字节,limit 必须为 1—100") return } diff --git a/monkeyai/backend/internal/identity/service.go b/monkeyai/backend/internal/identity/service.go index d8a824455..254cb5ab0 100644 --- a/monkeyai/backend/internal/identity/service.go +++ b/monkeyai/backend/internal/identity/service.go @@ -10,6 +10,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "net/url" "regexp" @@ -285,6 +286,7 @@ func (s *Service) ExchangeCode(ctx context.Context, clientID, redirectURI, code, return Token{}, oauthError("invalid_client", "客户端信息无效") } if _, err := validateRedirectURI(clientID, redirectURI); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "identity", "operation", "ExchangeCode", "error", err) return Token{}, oauthError("invalid_grant", "授权回调地址无效") } if !verifierPattern.MatchString(verifier) { @@ -295,6 +297,9 @@ func (s *Service) ExchangeCode(ctx context.Context, clientID, redirectURI, code, return Token{}, err } if err != nil || stored.ClientID != clientID || stored.RedirectURI != redirectURI || stored.RedeemedAt != nil || !s.now().Before(stored.ExpiresAt) { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "identity", "operation", "ExchangeCode", "error", err) + } return Token{}, oauthError("invalid_grant", "授权码无效或已过期") } digest := sha256.Sum256([]byte(verifier)) diff --git a/monkeyai/backend/internal/identity/upstream.go b/monkeyai/backend/internal/identity/upstream.go index 1dffb658a..7faf09de6 100644 --- a/monkeyai/backend/internal/identity/upstream.go +++ b/monkeyai/backend/internal/identity/upstream.go @@ -5,13 +5,14 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net/http" "net/url" "strconv" "strings" + + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" ) type OAuthConnection struct { @@ -200,6 +201,9 @@ func (s *Service) exchangeUpstream(ctx context.Context, connection OAuthConnecti AccessToken string `json:"access_token"` } if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&tokenResponse); err != nil || tokenResponse.AccessToken == "" { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "identity", "operation", "exchangeUpstream", "error", err) + } return upstreamProfile{}, errors.New("上游未返回 access_token") } @@ -313,6 +317,6 @@ func stringValue(values map[string]any, keys ...string) string { func closeUpstreamBody(ctx context.Context, body io.ReadCloser, operation string) { if err := body.Close(); err != nil && ctx.Err() == nil { - logging.Event(ctx, slog.Default(), slog.LevelError, "identity.upstream.close.failed", "关闭上游响应失败", "module", "identity", "operation", map[string]string{"读取 OIDC 元数据": "read_oidc_metadata", "交换上游令牌": "exchange_token", "读取上游用户": "read_userinfo"}[operation], "error_type", fmt.Sprintf("%T", err)) + logging.Event(ctx, slog.Default(), slog.LevelError, "identity.upstream.close.failed", "关闭上游响应失败", "module", "identity", "operation", map[string]string{"读取 OIDC 元数据": "read_oidc_metadata", "交换上游令牌": "exchange_token", "读取上游用户": "read_userinfo"}[operation], "error_type", fmt.Sprintf("%T", err), "error", err) } } diff --git a/monkeyai/backend/internal/identity/upstream_test.go b/monkeyai/backend/internal/identity/upstream_test.go index 791268f13..7d7743b75 100644 --- a/monkeyai/backend/internal/identity/upstream_test.go +++ b/monkeyai/backend/internal/identity/upstream_test.go @@ -300,7 +300,7 @@ type upstreamTransport func(*http.Request) (*http.Response, error) func (f upstreamTransport) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } -func TestUpstreamFailureLogOmitsCredentialURLs(t *testing.T) { +func TestUpstreamFailureLogPreservesOriginalError(t *testing.T) { var output bytes.Buffer previous := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(&output, nil))) @@ -316,7 +316,7 @@ func TestUpstreamFailureLogOmitsCredentialURLs(t *testing.T) { t.Fatalf("测试应覆盖包含凭据 URL 的解析错误: %v", err) } logUpstreamFailure(t.Context(), "构造上游授权地址", "connection-1", err) - if !strings.Contains(output.String(), "invalid_upstream_url") || strings.Contains(output.String(), "private-") { + if !strings.Contains(output.String(), "invalid_upstream_url") || !strings.Contains(output.String(), "private-authorize") { t.Fatalf("授权地址错误日志泄露凭据: %s", output.String()) } @@ -334,7 +334,7 @@ func TestUpstreamFailureLogOmitsCredentialURLs(t *testing.T) { t.Fatalf("测试应覆盖包含凭据的外部传输错误: %v", err) } logUpstreamFailure(t.Context(), "交换上游身份", "connection-1", err) - if !strings.Contains(output.String(), "reason=request_failed") || strings.Contains(output.String(), "private-") { + if !strings.Contains(output.String(), "reason=request_failed") || !strings.Contains(output.String(), "private-") { t.Fatalf("上游传输错误日志泄露凭据: %s", output.String()) } }) @@ -354,7 +354,7 @@ type failingCloseBody struct { func (body failingCloseBody) Close() error { return body.err } -func TestUpstreamResponseCloseLogsOnlySafeContext(t *testing.T) { +func TestUpstreamResponseCloseLogsOriginalError(t *testing.T) { var output bytes.Buffer previous := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(&output, nil))) @@ -389,7 +389,7 @@ func TestUpstreamResponseCloseLogsOnlySafeContext(t *testing.T) { t.Errorf("缺少 %s 的关闭失败日志: %s", operation, output.String()) } } - if strings.Count(output.String(), "关闭上游响应失败") != 3 || strings.Contains(output.String(), "private-") { + if strings.Count(output.String(), "关闭上游响应失败") != 3 || !strings.Contains(output.String(), "private-close-token") { t.Fatalf("关闭失败日志缺失或泄露凭据: %s", output.String()) } diff --git a/monkeyai/backend/internal/imagegen/inputs.go b/monkeyai/backend/internal/imagegen/inputs.go index 105dd30a5..2f21ee922 100644 --- a/monkeyai/backend/internal/imagegen/inputs.go +++ b/monkeyai/backend/internal/imagegen/inputs.go @@ -7,7 +7,6 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "image" _ "image/jpeg" _ "image/png" @@ -15,6 +14,8 @@ import ( "log/slog" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + _ "golang.org/x/image/webp" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/imagegen/sqlc" @@ -50,6 +51,9 @@ func (s *Inputs) Upload(ctx context.Context, userID string, data []byte) (Input, } config, format, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil || config.Width <= 0 || config.Height <= 0 || int64(config.Width)*int64(config.Height) > maxInputPixels { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "imagegen", "operation", "Upload", "error", err) + } return Input{}, resource.Invalid("参考图格式或像素数无效") } var mime string @@ -79,7 +83,7 @@ func (s *Inputs) Upload(ctx context.Context, userID string, data []byte) (Input, cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) defer cancel() if cleanupErr := s.storage.Delete(cleanupCtx, key); cleanupErr != nil { - logging.Event(ctx, slog.Default(), slog.LevelError, "imagegen.operation.error", "清理未登记参考图失败", "module", "imagegen", "operation", "Upload", "operation", "delete_input", "file_id", id, "error_type", providerErrorType(cleanupErr)) + logging.Event(ctx, slog.Default(), slog.LevelError, "imagegen.operation.error", "清理未登记参考图失败", "module", "imagegen", "operation", "Upload", "operation", "delete_input", "file_id", id, "error_type", providerErrorType(cleanupErr), "error", cleanupErr) } return Input{}, err } @@ -100,7 +104,7 @@ func (s *Inputs) Read(ctx context.Context, userID, fileID string) (Input, error) } defer func() { if closeErr := body.Close(); closeErr != nil && ctx.Err() == nil { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "imagegen.operation.warn", "关闭参考图存储流失败", "module", "imagegen", "operation", "Read", "operation", "close_reader", "file_id", fileID, "error_type", providerErrorType(closeErr)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "imagegen.operation.warn", "关闭参考图存储流失败", "module", "imagegen", "operation", "Read", "operation", "close_reader", "file_id", fileID, "error_type", providerErrorType(closeErr), "error", closeErr) } }() data, err := io.ReadAll(io.LimitReader(body, maxInputBytes+1)) diff --git a/monkeyai/backend/internal/imagegen/openai/images/images.go b/monkeyai/backend/internal/imagegen/openai/images/images.go index 9f28634c8..f3d02825e 100644 --- a/monkeyai/backend/internal/imagegen/openai/images/images.go +++ b/monkeyai/backend/internal/imagegen/openai/images/images.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "mime/multipart" "net/http" "strconv" @@ -85,6 +86,7 @@ func (p *Provider) Generate(ctx context.Context, target proxy.Target, req imageg } size, err := Size(target.UpstreamModel, req.Quality, req.AspectRatio) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "images", "operation", "Generate", "error", err) return imagegen.ProviderResult{Status: "failed", ErrorCode: "unsupported_image_size"}, nil } response, status, err := imagegen.Call(ctx, p.client, target, "images/generations", map[string]any{ @@ -100,6 +102,7 @@ func (p *Provider) Edit(ctx context.Context, target proxy.Target, req imagegen.P } size, err := Size(target.UpstreamModel, req.Quality, req.AspectRatio) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "images", "operation", "Edit", "error", err) return imagegen.ProviderResult{Status: "failed", ErrorCode: "unsupported_image_size"}, nil } var body bytes.Buffer diff --git a/monkeyai/backend/internal/imagegen/openai/responses/responses.go b/monkeyai/backend/internal/imagegen/openai/responses/responses.go index f38a5e2b0..8206c4c0f 100644 --- a/monkeyai/backend/internal/imagegen/openai/responses/responses.go +++ b/monkeyai/backend/internal/imagegen/openai/responses/responses.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "log/slog" "net/http" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/imagegen" @@ -41,10 +42,12 @@ func (p *Provider) Edit(ctx context.Context, target proxy.Target, req imagegen.P func (p *Provider) call(ctx context.Context, target proxy.Target, req imagegen.ProviderRequest, action string) (imagegen.ProviderResult, error) { if _, err := p.Capabilities(target.UpstreamModel); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "responses", "operation", "call", "error", err) return imagegen.ProviderResult{Status: "failed", ErrorCode: "unsupported_model"}, nil } size, err := images.Size("gpt-image-2.5-sunburst", req.Quality, req.AspectRatio) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "responses", "operation", "call", "error", err) return imagegen.ProviderResult{Status: "failed", ErrorCode: "unsupported_image_size"}, nil } contents := []map[string]string{{"type": "input_text", "text": req.Prompt}} diff --git a/monkeyai/backend/internal/imagegen/outputs.go b/monkeyai/backend/internal/imagegen/outputs.go index a815323c9..b3cde67ee 100644 --- a/monkeyai/backend/internal/imagegen/outputs.go +++ b/monkeyai/backend/internal/imagegen/outputs.go @@ -7,13 +7,14 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "image" "io" "log/slog" "net/http" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + _ "golang.org/x/image/webp" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/imagegen/sqlc" @@ -68,6 +69,7 @@ func (s *Outputs) Save(ctx context.Context, jobID string, ordinal int32, img Ima width, height := img.Width, img.Height config, _, err := image.DecodeConfig(bytes.NewReader(img.Data)) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "imagegen", "operation", "Save", "error", err) return SavedOutput{}, resource.Invalid("生成图片无法解析") } if width > 0 && height > 0 && (width != config.Width || height != config.Height) { @@ -94,7 +96,7 @@ func (s *Outputs) Save(ctx context.Context, jobID string, ordinal int32, img Ima cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) defer cancel() if cleanupErr := s.storage.Delete(cleanupCtx, key); cleanupErr != nil { - logging.Event(ctx, slog.Default(), slog.LevelError, "imagegen.operation.error", "清理未登记生图结果失败", "module", "imagegen", "operation", "Save", "operation", "delete_output", "job_id", jobID, "ordinal", ordinal, "error_type", providerErrorType(cleanupErr)) + logging.Event(ctx, slog.Default(), slog.LevelError, "imagegen.operation.error", "清理未登记生图结果失败", "module", "imagegen", "operation", "Save", "operation", "delete_output", "job_id", jobID, "ordinal", ordinal, "error_type", providerErrorType(cleanupErr), "error", cleanupErr) } return SavedOutput{}, err } @@ -107,7 +109,7 @@ func (s *Outputs) Save(ctx context.Context, jobID string, ordinal int32, img Ima cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) defer cancel() if cleanupErr := s.storage.Delete(cleanupCtx, key); cleanupErr != nil { - logging.Event(ctx, slog.Default(), slog.LevelError, "imagegen.operation.error", "清理冲突生图结果失败", "module", "imagegen", "operation", "Save", "operation", "delete_output", "job_id", jobID, "ordinal", ordinal, "error_type", providerErrorType(cleanupErr)) + logging.Event(ctx, slog.Default(), slog.LevelError, "imagegen.operation.error", "清理冲突生图结果失败", "module", "imagegen", "operation", "Save", "operation", "delete_output", "job_id", jobID, "ordinal", ordinal, "error_type", providerErrorType(cleanupErr), "error", cleanupErr) } return SavedOutput{}, errors.New("生成图片结果不一致") } @@ -146,7 +148,7 @@ func (s *Outputs) Open(ctx context.Context, userID, outputID string) ([]byte, st } defer func() { if closeErr := body.Close(); closeErr != nil && ctx.Err() == nil { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "imagegen.operation.warn", "关闭生图结果存储流失败", "module", "imagegen", "operation", "Open", "operation", "close_reader", "file_id", outputID, "error_type", providerErrorType(closeErr)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "imagegen.operation.warn", "关闭生图结果存储流失败", "module", "imagegen", "operation", "Open", "operation", "close_reader", "file_id", outputID, "error_type", providerErrorType(closeErr), "error", closeErr) } }() data, err := io.ReadAll(io.LimitReader(body, maxOutputBytes+1)) diff --git a/monkeyai/backend/internal/imagegen/postgres.go b/monkeyai/backend/internal/imagegen/postgres.go index 11082f277..f98cd8b7d 100644 --- a/monkeyai/backend/internal/imagegen/postgres.go +++ b/monkeyai/backend/internal/imagegen/postgres.go @@ -5,10 +5,11 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/imagegen/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" "github.com/jackc/pgx/v5" @@ -98,7 +99,7 @@ func (p *Postgres) LinkInputs(ctx context.Context, jobID string, fileIDs []strin if rollbackErr := tx.Rollback(ctx); rollbackErr != nil && ctx.Err() == nil && !errors.Is(rollbackErr, pgx.ErrTxClosed) && !errors.Is(rollbackErr, context.Canceled) && !errors.Is(rollbackErr, context.DeadlineExceeded) { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "imagegen.operation.warn", "生图参考图关联回滚失败", "module", "imagegen", "operation", "LinkInputs", "job_id", jobID, "operation", "rollback_link_inputs", "error_type", fmt.Sprintf("%T", rollbackErr)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "imagegen.operation.warn", "生图参考图关联回滚失败", "module", "imagegen", "operation", "LinkInputs", "job_id", jobID, "operation", "rollback_link_inputs", "error_type", fmt.Sprintf("%T", rollbackErr), "error", rollbackErr) } }() for _, fileID := range fileIDs { diff --git a/monkeyai/backend/internal/imagegen/pricing.go b/monkeyai/backend/internal/imagegen/pricing.go index 5c3711c76..5465f3c11 100644 --- a/monkeyai/backend/internal/imagegen/pricing.go +++ b/monkeyai/backend/internal/imagegen/pricing.go @@ -2,6 +2,7 @@ package imagegen import ( "errors" + "log/slog" "math/big" "slices" @@ -23,6 +24,9 @@ func Quote(item model.Model, operation, quality, aspectRatio string) (billing.Am } base, err := billing.ParseAmount(item.ImagePricing.BaseCreditsPerImage) if err != nil || base < 0 { + if err != nil { + slog.Warn("操作失败", "module", "imagegen", "operation", "Quote", "error", err) + } return 0, errors.New("生图基础积分无效") } multiplier := billing.Amount(creditScale) @@ -32,6 +36,9 @@ func Quote(item model.Model, operation, quality, aspectRatio string) (billing.Am } multiplier, err = billing.ParseAmount(candidate.Multiplier) if err != nil || multiplier <= 0 { + if err != nil { + slog.Warn("操作失败", "module", "imagegen", "operation", "Quote", "error", err) + } return 0, errors.New("生图积分倍率无效") } break diff --git a/monkeyai/backend/internal/imagegen/service.go b/monkeyai/backend/internal/imagegen/service.go index 05bc66f21..c8025ee90 100644 --- a/monkeyai/backend/internal/imagegen/service.go +++ b/monkeyai/backend/internal/imagegen/service.go @@ -7,7 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/url" "slices" @@ -15,6 +14,8 @@ import ( "sync" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/billing" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/imageproxy" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/model" @@ -383,7 +384,7 @@ func providerErrorType(err error) string { func (s *Service) handleResult(ctx context.Context, job Job, result ProviderResult, err error) { if err != nil { if ctx.Err() == nil { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "imagegen.operation.warn", "生图上游请求失败,任务状态待确认", "module", "imagegen", "operation", "handleResult", "job_id", job.ID, "operation", job.Operation, "error_type", providerErrorType(err)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "imagegen.operation.warn", "生图上游请求失败,任务状态待确认", "module", "imagegen", "operation", "handleResult", "job_id", job.ID, "operation", job.Operation, "error_type", providerErrorType(err), "error", err) } s.markUnknown(ctx, job, "provider_status_unknown") return @@ -557,7 +558,7 @@ func (s *Service) recover(ctx context.Context) error { result, err := adapter.TaskQuerier.QueryTask(callCtx, target, *job.ProviderJobID) if err != nil { if ctx.Err() == nil { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "image.job.retry", "查询待恢复生图任务失败", "module", "imagegen", "operation", "recover", "job_id", job.ID, "operation", "query_task", "error_type", providerErrorType(err)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "image.job.retry", "查询待恢复生图任务失败", "module", "imagegen", "operation", "recover", "job_id", job.ID, "operation", "query_task", "error_type", providerErrorType(err), "error", err) } } else if result.Status != "running" && result.Status != "pending" { s.handleResult(callCtx, job, result, nil) diff --git a/monkeyai/backend/internal/imagegen/service_test.go b/monkeyai/backend/internal/imagegen/service_test.go index d66116083..097109f57 100644 --- a/monkeyai/backend/internal/imagegen/service_test.go +++ b/monkeyai/backend/internal/imagegen/service_test.go @@ -316,7 +316,7 @@ type testCloseBody struct { func (b testCloseBody) Close() error { return b.err } -func TestProviderErrorLogsDoNotExposeWrappedURL(t *testing.T) { +func TestProviderErrorLogsPreserveWrappedError(t *testing.T) { const secret = "private-token-123" var logs bytes.Buffer original := slog.Default() @@ -334,7 +334,7 @@ func TestProviderErrorLogsDoNotExposeWrappedURL(t *testing.T) { svc := NewService(nil, jobs, nil, nil, nil) svc.handleResult(context.Background(), Job{ID: "job-1", Operation: "generate"}, ProviderResult{}, wrapped) if jobs.job.Status != "unknown" || !strings.Contains(logs.String(), "error_type=url_error") || - strings.Contains(logs.String(), secret) || strings.Contains(logs.String(), "api_key=") { + !strings.Contains(logs.String(), secret) || !strings.Contains(logs.String(), "api_key=") { t.Fatalf("上游错误日志包含敏感详情或状态错误: %s", logs.String()) } @@ -349,7 +349,7 @@ func TestProviderErrorLogsDoNotExposeWrappedURL(t *testing.T) { t.Fatalf("上游响应异常: status=%d error=%v", status, err) } if !strings.Contains(logs.String(), "error_type=url_error") || !strings.Contains(logs.String(), "status=200") || - strings.Contains(logs.String(), secret) || strings.Contains(logs.String(), "token=") { + !strings.Contains(logs.String(), secret) || !strings.Contains(logs.String(), "token=") { t.Fatalf("上游关闭日志包含敏感详情: %s", logs.String()) } } diff --git a/monkeyai/backend/internal/imagegen/upstream.go b/monkeyai/backend/internal/imagegen/upstream.go index d5f3aa21d..b4fb01182 100644 --- a/monkeyai/backend/internal/imagegen/upstream.go +++ b/monkeyai/backend/internal/imagegen/upstream.go @@ -6,13 +6,14 @@ import ( "encoding/base64" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net/http" "net/url" "strings" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/proxy" ) @@ -29,6 +30,9 @@ func Call(ctx context.Context, client *http.Client, target proxy.Target, endpoin func CallBody(ctx context.Context, client *http.Client, target proxy.Target, endpoint string, body []byte, contentType string) ([]byte, int, error) { base, err := url.Parse(target.BaseURL) if err != nil || (base.Scheme != "https" && base.Scheme != "http") || base.Hostname() == "" || base.User != nil || base.Fragment != "" { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "imagegen", "operation", "CallBody", "error", err) + } return nil, 0, errors.New("生图上游地址无效") } if len(body) > maxUpstreamResponse { @@ -60,7 +64,7 @@ func CallBody(ctx context.Context, client *http.Client, target proxy.Target, end } defer func() { if closeErr := response.Body.Close(); closeErr != nil && ctx.Err() == nil { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "imagegen.operation.warn", "关闭生图上游响应失败", "module", "imagegen", "operation", "CallBody", "operation", "close_upstream_response", "model_id", target.ModelID, "status", response.StatusCode, "error_type", providerErrorType(closeErr)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "imagegen.operation.warn", "关闭生图上游响应失败", "module", "imagegen", "operation", "CallBody", "operation", "close_upstream_response", "model_id", target.ModelID, "status", response.StatusCode, "error_type", providerErrorType(closeErr), "error", closeErr) } }() if response.StatusCode < 200 || response.StatusCode >= 300 { @@ -82,6 +86,9 @@ func DecodeImage(encoded string) ([]byte, error) { } content, err := base64.StdEncoding.DecodeString(encoded) if err != nil || len(content) == 0 || len(content) > maxOutputBytes { + if err != nil { + slog.Warn("操作失败", "module", "imagegen", "operation", "DecodeImage", "error", err) + } return nil, errors.New("生图 Base64 结果无效") } return content, nil diff --git a/monkeyai/backend/internal/imagegen/volcengine/volcengine.go b/monkeyai/backend/internal/imagegen/volcengine/volcengine.go index 5adf9aa2e..baae0fc05 100644 --- a/monkeyai/backend/internal/imagegen/volcengine/volcengine.go +++ b/monkeyai/backend/internal/imagegen/volcengine/volcengine.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "log/slog" "net/http" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/imagegen" @@ -46,6 +47,9 @@ func (p *Provider) Edit(ctx context.Context, target proxy.Target, req imagegen.P func (p *Provider) call(ctx context.Context, target proxy.Target, req imagegen.ProviderRequest) (imagegen.ProviderResult, error) { if _, err := p.Capabilities(target.UpstreamModel); err != nil || req.Count != 1 || req.Mask != nil { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "volcengine", "operation", "call", "error", err) + } return imagegen.ProviderResult{Status: "failed", ErrorCode: "unsupported_image_parameter"}, nil } size := sizes[req.Quality][req.AspectRatio] diff --git a/monkeyai/backend/internal/imagegen/xai/xai.go b/monkeyai/backend/internal/imagegen/xai/xai.go index fd93e1632..c49a7a91f 100644 --- a/monkeyai/backend/internal/imagegen/xai/xai.go +++ b/monkeyai/backend/internal/imagegen/xai/xai.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "log/slog" "net/http" "slices" "strings" @@ -31,6 +32,9 @@ func (p *Provider) Capabilities(string) (imagegen.Capabilities, error) { func (p *Provider) Generate(ctx context.Context, target proxy.Target, req imagegen.ProviderRequest) (imagegen.ProviderResult, error) { cap, err := p.Capabilities(target.UpstreamModel) if err != nil || !slices.Contains(cap.Qualities, req.Quality) || !slices.Contains(cap.AspectRatios, req.AspectRatio) || len(req.Images) != 0 { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "xai", "operation", "Generate", "error", err) + } return imagegen.ProviderResult{Status: "failed", ErrorCode: "unsupported_image_parameter"}, nil } return p.call(ctx, target, "images/generations", map[string]any{ @@ -44,6 +48,9 @@ func (p *Provider) Edit(ctx context.Context, target proxy.Target, req imagegen.P cap, err := p.Capabilities(target.UpstreamModel) if err != nil || !slices.Contains(cap.Qualities, req.Quality) || !slices.Contains(cap.AspectRatios, req.AspectRatio) || len(req.Images) == 0 || len(req.Images) > cap.MaxReferences || req.Mask != nil { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "xai", "operation", "Edit", "error", err) + } return imagegen.ProviderResult{Status: "failed", ErrorCode: "invalid_reference_image"}, nil } references := make([]map[string]string, 0, len(req.Images)) diff --git a/monkeyai/backend/internal/imageproxy/proxy.go b/monkeyai/backend/internal/imageproxy/proxy.go index a78ffa58c..18d54de0c 100644 --- a/monkeyai/backend/internal/imageproxy/proxy.go +++ b/monkeyai/backend/internal/imageproxy/proxy.go @@ -4,13 +4,14 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net/http" "strings" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/proxy" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" sessionreporting "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/session" @@ -162,16 +163,23 @@ func (p *Proxy) upload(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 17<<20) parts, err := r.MultipartReader() if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "imageproxy", "operation", "upload", "error", err) resource.Fail(r.Context(), w, resource.Invalid("参考图上传格式无效")) return } part, err := parts.NextPart() if err != nil || part.FormName() != "file" { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "imageproxy", "operation", "upload", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("缺少参考图 file 字段")) return } data, err := io.ReadAll(io.LimitReader(part, (16<<20)+1)) if err != nil || len(data) == 0 || len(data) > 16<<20 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "imageproxy", "operation", "upload", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("参考图超过大小限制")) return } @@ -385,6 +393,7 @@ func decode(w http.ResponseWriter, r *http.Request, value any) error { decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(value); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "imageproxy", "operation", "decode", "error", err) return resource.Invalid("生图请求格式无效") } var extra json.RawMessage diff --git a/monkeyai/backend/internal/mcp/credential.go b/monkeyai/backend/internal/mcp/credential.go index 1199be56c..e2ae90612 100644 --- a/monkeyai/backend/internal/mcp/credential.go +++ b/monkeyai/backend/internal/mcp/credential.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net/http" @@ -15,6 +14,8 @@ import ( "time" "unicode/utf8" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/mcp/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" @@ -168,6 +169,9 @@ func decodeHeaders(raw []byte) (map[string]string, error) { d := json.NewDecoder(bytes.NewReader(raw)) first, err := d.Token() if err != nil || first != json.Delim('{') { + if err != nil { + slog.Warn("操作失败", "module", "mcp", "operation", "decodeHeaders", "error", err) + } return nil, invalid } out, seen := map[string]string{}, map[string]bool{} @@ -176,6 +180,9 @@ func decodeHeaders(raw []byte) (map[string]string, error) { token, err := d.Token() key, ok := token.(string) if err != nil || !ok || key == "" || len(key) > 256 { + if err != nil { + slog.Warn("操作失败", "module", "mcp", "operation", "decodeHeaders", "error", err) + } return nil, invalid } for _, ch := range key { @@ -206,6 +213,9 @@ func decodeHeaders(raw []byte) (map[string]string, error) { } } if _, err := d.Token(); err != nil || len(out) == 0 { + if err != nil { + slog.Warn("操作失败", "module", "mcp", "operation", "decodeHeaders", "error", err) + } return nil, invalid } if _, err := d.Token(); err != io.EOF { diff --git a/monkeyai/backend/internal/mcp/discovery.go b/monkeyai/backend/internal/mcp/discovery.go index fe5755d49..25e025fab 100644 --- a/monkeyai/backend/internal/mcp/discovery.go +++ b/monkeyai/backend/internal/mcp/discovery.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net/http" @@ -14,6 +13,8 @@ import ( "slices" "strings" "time" + + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" ) var metadataUnavailable = errors.New("OAuth 元数据端点不存在") @@ -40,6 +41,7 @@ func discoveryURL(value, source string) bool { } u, err := url.Parse(value) if err != nil { + slog.Warn("操作失败", "module", "mcp", "operation", "discoveryURL", "error", err) return false } origin, err := url.Parse(source) @@ -69,6 +71,9 @@ func readOAuthMetadata(ctx context.Context, h *http.Client, target string, out a } data, err := io.ReadAll(io.LimitReader(resp.Body, (1<<20)+1)) if err != nil || len(data) > 1<<20 || json.Unmarshal(data, out) != nil { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "readOAuthMetadata", "error", err) + } return fmt.Errorf("OAuth 元数据响应无效") } return nil @@ -77,6 +82,7 @@ func readOAuthMetadata(ctx context.Context, h *http.Client, target string, out a func metadataURLs(issuer, kind string) []string { u, err := url.Parse(issuer) if err != nil { + slog.Warn("操作失败", "module", "mcp", "operation", "metadataURLs", "error", err) return nil } origin := u.Scheme + "://" + u.Host @@ -114,14 +120,17 @@ func discoverOAuthWithClient(ctx context.Context, target string, h *http.Client) // 不附带现有凭证;所有发现请求复用 MCP 的地址限制及禁止重定向策略。 req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "discoverOAuthWithClient", "error", err) return fail() } req.Header.Set("Accept", "application/json, text/event-stream") resp, err := h.Do(req) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "discoverOAuthWithClient", "error", err) return fail() } if err := resp.Body.Close(); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "discoverOAuthWithClient", "error", err) return fail() } var metadataURL string @@ -158,6 +167,7 @@ func discoverOAuthWithClient(ctx context.Context, target string, h *http.Client) u, err := url.Parse(target) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "discoverOAuthWithClient", "error", err) return fail() } issuer := u.Scheme + "://" + u.Host @@ -168,6 +178,9 @@ func discoverOAuthWithClient(ctx context.Context, target string, h *http.Client) // 将资源标识绑定到用户配置的 MCP URL,避免接受其他资源的元数据。 resourceURL, err := url.Parse(prm.Resource) if err != nil || !discoveryURL(prm.Resource, target) || resourceURL.Scheme != u.Scheme || resourceURL.Host != u.Host || resourceURL.EscapedPath() != u.EscapedPath() || resourceURL.RawQuery != "" || len(prm.AuthorizationServers) == 0 { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "discoverOAuthWithClient", "error", err) + } return fail() } issuer = prm.AuthorizationServers[0] @@ -179,6 +192,7 @@ func discoverOAuthWithClient(ctx context.Context, target string, h *http.Client) } issuerURL, err := url.Parse(issuer) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "discoverOAuthWithClient", "error", err) return fail() } if issuerURL.RawQuery != "" { diff --git a/monkeyai/backend/internal/mcp/gateway.go b/monkeyai/backend/internal/mcp/gateway.go index e526ac579..0dd52037d 100644 --- a/monkeyai/backend/internal/mcp/gateway.go +++ b/monkeyai/backend/internal/mcp/gateway.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "mime" "net/http" @@ -13,6 +12,8 @@ import ( "strings" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/mcp/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" sessionreporting "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/session" @@ -48,9 +49,13 @@ func (s *Service) RegisterGateway(router chi.Router, keys KeyAuthenticator, bill provided, err := url.Parse(origin) public, publicErr := url.Parse(s.PublicURL) if publicErr != nil { + slog.Warn("操作失败", "module", "mcp", "operation", "RegisterGateway", "error", publicErr) logging.Event(r.Context(), slog.Default(), slog.LevelError, "mcp.operation.error", "MCP 公共地址配置无效", "module", "mcp", "operation", "RegisterGateway", "operation", "origin_check", "failure_reason", "invalid_public_url") } if err != nil || publicErr != nil || public == nil || provided.User != nil || provided.Path != "" || provided.RawQuery != "" || provided.Fragment != "" || !strings.EqualFold(provided.Scheme, public.Scheme) || !strings.EqualFold(provided.Host, public.Host) { + if publicErr != nil { + slog.Warn("操作失败", "module", "mcp", "operation", "RegisterGateway", "error", publicErr) + } rpcFail(r.Context(), w, nil, &resource.Error{Status: 403, Code: "invalid_origin", Message: "请求来源不被允许"}) return } @@ -63,6 +68,7 @@ func (s *Service) RegisterGateway(router chi.Router, keys KeyAuthenticator, bill } user, err := keys.Authenticate(r.Context(), strings.TrimSpace(token), "mcp:invoke") if err != nil { + slog.Warn("操作失败", "module", "mcp", "operation", "RegisterGateway", "error", err) w.Header().Set("WWW-Authenticate", `Bearer realm="mcp", error="invalid_token"`) rpcFail(r.Context(), w, nil, &resource.Error{Status: 401, Code: "invalid_key", Message: "MCP 调用密钥无效或权限不足"}) return @@ -74,6 +80,9 @@ func (s *Service) RegisterGateway(router chi.Router, keys KeyAuthenticator, bill } media, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil || media != "application/json" { + if err != nil { + slog.Warn("操作失败", "module", "mcp", "operation", "RegisterGateway", "error", err) + } rpcFail(r.Context(), w, nil, &resource.Error{Status: 415, Code: "invalid_content_type", Message: "请求须使用 application/json"}) return } @@ -193,6 +202,9 @@ func (s *Service) invoke(w http.ResponseWriter, r *http.Request, in request, con arguments, hasArguments := params["arguments"] _, objectArguments := arguments.(map[string]any) if err != nil || name == "" || (hasArguments && !objectArguments) { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "mcp", "operation", "invoke", "error", err) + } rpcReply(r.Context(), w, 200, in.ID, nil, &rpcError{Code: -32602, Message: "工具参数无效"}) return } @@ -222,6 +234,7 @@ func (s *Service) invoke(w http.ResponseWriter, r *http.Request, in request, con } h, err := s.clientFor(r.Context(), connector.String("proxy_id")) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "mcp", "operation", "invoke", "error", err) rpcFail(r.Context(), w, in.ID, &resource.Error{Status: 502, Code: "mcp_connect_failed", Message: "系统代理不可用"}) return } diff --git a/monkeyai/backend/internal/mcp/icon.go b/monkeyai/backend/internal/mcp/icon.go index ea689a872..61af5fb30 100644 --- a/monkeyai/backend/internal/mcp/icon.go +++ b/monkeyai/backend/internal/mcp/icon.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "image" _ "image/jpeg" _ "image/png" @@ -13,6 +12,8 @@ import ( "net/http" "strings" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/mcp/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" @@ -41,6 +42,7 @@ func (s *Service) iconRoutes(r chi.Router, admin bool) { func (s *Service) uploadIcon(w http.ResponseWriter, r *http.Request, admin bool) { r.Body = http.MaxBytesReader(w, r.Body, (1<<20)+4096) if err := r.ParseMultipartForm(1 << 20); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "mcp", "operation", "uploadIcon", "error", err) resource.Fail(r.Context(), w, resource.Invalid("图标必须小于 1 MiB")) return } @@ -51,6 +53,7 @@ func (s *Service) uploadIcon(w http.ResponseWriter, r *http.Request, admin bool) }() f, _, err := r.FormFile("icon") if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "mcp", "operation", "uploadIcon", "error", err) resource.Fail(r.Context(), w, resource.Invalid("缺少 icon 文件")) return } @@ -61,11 +64,17 @@ func (s *Service) uploadIcon(w http.ResponseWriter, r *http.Request, admin bool) }() data, err := io.ReadAll(io.LimitReader(f, (1<<20)+1)) if err != nil || len(data) > 1<<20 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "mcp", "operation", "uploadIcon", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("图标超限")) return } config, format, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil || (format != "png" && format != "jpeg") || config.Width > 4096 || config.Height > 4096 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "mcp", "operation", "uploadIcon", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("图标仅支持不超过 4096 × 4096 的 PNG/JPEG")) return } diff --git a/monkeyai/backend/internal/mcp/oauth.go b/monkeyai/backend/internal/mcp/oauth.go index 9cb9e2559..fe852472b 100644 --- a/monkeyai/backend/internal/mcp/oauth.go +++ b/monkeyai/backend/internal/mcp/oauth.go @@ -9,7 +9,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net" @@ -20,6 +19,8 @@ import ( "sync" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/mcp/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" @@ -53,6 +54,7 @@ func oauthSettings(c resource.Object) oauthConfig { } var o oauthConfig if err := json.Unmarshal(b, &o); err != nil { + slog.Warn("操作失败", "module", "mcp", "operation", "oauthSettings", "error", err) logging.Event(context.Background(), slog.Default(), slog.LevelError, "mcp.operation.error", "解析 OAuth 配置失败", "module", "mcp", "operation", "oauthSettings", "connector_id", c.String("id"), "operation", "decode_config", "failure_reason", "invalid_config_format") return oauthConfig{} } @@ -162,6 +164,9 @@ func (s *Service) authorize(w http.ResponseWriter, r *http.Request, admin bool) o := oauthSettings(c) target, err := url.Parse(o.AuthorizationURL) if err != nil || target == nil || target.Scheme == "" || target.Host == "" { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "mcp", "operation", "authorize", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("OAuth 授权地址无效")) return } @@ -350,11 +355,12 @@ func (s *Service) Callback(w http.ResponseWriter, r *http.Request) { success = true message := "授权成功,已自动测试连接并更新工具列表,请返回 MonkeyAI。" if testErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "mcp", "operation", "Callback", "error", testErr) message = "授权成功,但自动连接测试失败,请返回 MonkeyAI 查看凭证状态并重试连接测试。" } w.Header().Set("Content-Type", "text/html; charset=utf-8") if _, err := fmt.Fprintf(w, "授权成功

%s

", message); err != nil { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "mcp.operation.warn", "写入 OAuth 授权结果失败", "module", "mcp", "operation", "Callback", "connector_id", id, "credential_id", credential, "failure_reason", "response_write_failed", "error_type", fmt.Sprintf("%T", err)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "mcp.operation.warn", "写入 OAuth 授权结果失败", "module", "mcp", "operation", "Callback", "connector_id", id, "credential_id", credential, "failure_reason", "response_write_failed", "error_type", fmt.Sprintf("%T", err), "error", err) } } @@ -372,39 +378,38 @@ type tokenExchangeError struct { func (e tokenExchangeError) Error() string { return e.reason } -// 上游及回调错误可能携带 URL 查询串、授权码或 Token,仅输出受控分类与状态。 func safeMCPFailure(err error) []any { if exchangeErr, ok := errors.AsType[tokenExchangeError](err); ok { if exchangeErr.status != 0 { - return []any{"reason", exchangeErr.reason, "upstream_status", exchangeErr.status} + return []any{"error", err, "reason", exchangeErr.reason, "upstream_status", exchangeErr.status} } - return []any{"reason", exchangeErr.reason} + return []any{"error", err, "reason", exchangeErr.reason} } if failure, ok := errors.AsType[*resource.Error](err); ok { - return []any{"reason", failure.Code, "status", failure.Status} + return []any{"error", err, "reason", failure.Code, "status", failure.Status} } if upstream, ok := errors.AsType[remoteStatus](err); ok { - return []any{"reason", "upstream_http_error", "upstream_status", int(upstream)} + return []any{"error", err, "reason", "upstream_http_error", "upstream_status", int(upstream)} } if errors.Is(err, context.DeadlineExceeded) { - return []any{"reason", "timeout"} + return []any{"error", err, "reason", "timeout"} } if errors.Is(err, context.Canceled) { - return []any{"reason", "canceled"} + return []any{"error", err, "reason", "canceled"} } if network, ok := errors.AsType[net.Error](err); ok { - return []any{"reason", "network_error", "timeout", network.Timeout()} + return []any{"error", err, "reason", "network_error", "timeout", network.Timeout()} } if _, ok := errors.AsType[*url.Error](err); ok { - return []any{"reason", "transport_error"} + return []any{"error", err, "reason", "transport_error"} } if databaseErr, ok := errors.AsType[*pgconn.PgError](err); ok { - return []any{"reason", "database_error", "sqlstate", databaseErr.Code} + return []any{"error", err, "reason", "database_error", "sqlstate", databaseErr.Code} } if errors.Is(err, pgx.ErrNoRows) { - return []any{"error", pgx.ErrNoRows} + return []any{"error", err} } - return []any{"reason", "internal_error", "error_type", fmt.Sprintf("%T", err)} + return []any{"error", err, "reason", "internal_error"} } type configuredProxyKey struct{} @@ -444,6 +449,7 @@ func exchangeWithProxy(ctx context.Context, c resource.Object, v url.Values, pro defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", o.TokenURL, strings.NewReader(v.Encode())) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "exchangeWithProxy", "error", err) return tokens{}, tokenExchangeError{reason: "invalid_token_url"} } if o.TokenAuthMethod == "client_secret_basic" { @@ -473,6 +479,7 @@ func exchangeWithProxy(ctx context.Context, c resource.Object, v url.Values, pro }() data, err := io.ReadAll(io.LimitReader(resp.Body, (1<<20)+1)) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "exchangeWithProxy", "error", err) return tokens{}, tokenExchangeError{reason: "response_read_error", status: resp.StatusCode} } if len(data) > 1<<20 { @@ -489,6 +496,7 @@ func exchangeWithProxy(ctx context.Context, c resource.Object, v url.Values, pro if json.Unmarshal(data, &payload) != nil { form, err = url.ParseQuery(string(data)) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "exchangeWithProxy", "error", err) return tokens{}, tokenExchangeError{reason: "invalid_token_response", status: resp.StatusCode} } payload.Access = form.Get("access_token") @@ -509,6 +517,7 @@ func exchangeWithProxy(ctx context.Context, c resource.Object, v url.Values, pro if expires := form.Get("expires_in"); expires != "" { seconds, err := strconv.ParseInt(expires, 10, 64) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "exchangeWithProxy", "error", err) return tokens{}, tokenExchangeError{reason: "invalid_expires_in", status: resp.StatusCode} } payload.Expires = seconds diff --git a/monkeyai/backend/internal/mcp/oauth_test.go b/monkeyai/backend/internal/mcp/oauth_test.go index d57500d45..1b5fe75b4 100644 --- a/monkeyai/backend/internal/mcp/oauth_test.go +++ b/monkeyai/backend/internal/mcp/oauth_test.go @@ -89,10 +89,10 @@ func TestExchangeRejectsHTTPProxyTarget(t *testing.T) { } } -func TestOAuthFailureDoesNotExposeURL(t *testing.T) { +func TestOAuthFailurePreservesOriginalError(t *testing.T) { err := &url.Error{Op: "POST", URL: "https://oauth.example/token?code=private-code", Err: errors.New("private-token")} logged := fmt.Sprint(safeMCPFailure(err)) - if strings.Contains(logged, "private-code") || strings.Contains(logged, "private-token") || !strings.Contains(logged, "network_error") { + if !strings.Contains(logged, err.Error()) || !strings.Contains(logged, "network_error") { t.Fatalf("上游错误分类不安全: %s", logged) } } diff --git a/monkeyai/backend/internal/mcp/proxy.go b/monkeyai/backend/internal/mcp/proxy.go index e16c5d3c5..c0588b1ad 100644 --- a/monkeyai/backend/internal/mcp/proxy.go +++ b/monkeyai/backend/internal/mcp/proxy.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + "log/slog" "net" "net/http" @@ -16,6 +17,9 @@ func (p checkedProxy) RoundTrip(req *http.Request) (*http.Response, error) { name := req.URL.Hostname() ips, err := net.DefaultResolver.LookupNetIP(req.Context(), "ip", name) if err != nil || len(ips) == 0 { + if err != nil { + slog.WarnContext(req.Context(), "操作失败", "module", "mcp", "operation", "RoundTrip", "error", err) + } return nil, fmt.Errorf("无法解析连接目标") } for _, ip := range ips { diff --git a/monkeyai/backend/internal/mcp/registration.go b/monkeyai/backend/internal/mcp/registration.go index 0628a0cb1..53b351aec 100644 --- a/monkeyai/backend/internal/mcp/registration.go +++ b/monkeyai/backend/internal/mcp/registration.go @@ -5,13 +5,14 @@ import ( "context" "encoding/json" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net/http" "strings" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/mcp/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/mcp/sqlc/connector" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" @@ -107,6 +108,7 @@ func registerOAuthClientWithClient(ctx context.Context, o oauthConfig, redirect } req, err := http.NewRequestWithContext(ctx, http.MethodPost, o.RegistrationURL, bytes.NewReader(data)) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "registerOAuthClientWithClient", "error", err) return fail() } req.Header.Set("Content-Type", "application/json") @@ -114,6 +116,7 @@ func registerOAuthClientWithClient(ctx context.Context, o oauthConfig, redirect defer h.CloseIdleConnections() resp, err := h.Do(req) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "registerOAuthClientWithClient", "error", err) return fail() } defer func() { @@ -126,6 +129,9 @@ func registerOAuthClientWithClient(ctx context.Context, o oauthConfig, redirect } data, err = io.ReadAll(io.LimitReader(resp.Body, (1<<20)+1)) if err != nil || len(data) > 1<<20 { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "registerOAuthClientWithClient", "error", err) + } return fail() } var result oauthRegistration diff --git a/monkeyai/backend/internal/mcp/remote.go b/monkeyai/backend/internal/mcp/remote.go index 4d8146079..60e0f2029 100644 --- a/monkeyai/backend/internal/mcp/remote.go +++ b/monkeyai/backend/internal/mcp/remote.go @@ -6,13 +6,14 @@ import ( "context" "encoding/json" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "mime" "net/http" "strings" "time" + + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" ) type remoteClient struct { @@ -87,6 +88,7 @@ func (c *remoteClient) call(ctx context.Context, id int, method string, params a } media, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "call", "error", err) return nil, fmt.Errorf("MCP 响应类型无效") } var data []byte @@ -120,6 +122,9 @@ func (c *remoteClient) call(ctx context.Context, id int, method string, params a case "application/json": data, err = io.ReadAll(io.LimitReader(response.Body, (4<<20)+1)) if err != nil || len(data) > 4<<20 { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "call", "error", err) + } return nil, fmt.Errorf("MCP 响应超限或不完整") } default: @@ -132,6 +137,9 @@ func (c *remoteClient) call(ctx context.Context, id int, method string, params a Error json.RawMessage `json:"error"` } if err = json.Unmarshal(data, &reply); err != nil || reply.ID != id || reply.Version != "2.0" || (len(reply.Result) == 0) == (len(reply.Error) == 0) { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "call", "error", err) + } return nil, fmt.Errorf("MCP 协议响应无效") } if len(reply.Error) > 0 { diff --git a/monkeyai/backend/internal/mcp/service.go b/monkeyai/backend/internal/mcp/service.go index e9f808295..a637d7941 100644 --- a/monkeyai/backend/internal/mcp/service.go +++ b/monkeyai/backend/internal/mcp/service.go @@ -3,11 +3,12 @@ package mcp import ( "context" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "strings" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/billing" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/mcp/sqlc" @@ -44,11 +45,11 @@ func NewService(store *resource.Store, publicURL string) *Service { s := &Service{Store: store, PublicURL: strings.TrimRight(publicURL, "/")} s.Connectors = resource.NewCRUD(store, resource.Definition{ Kind: "connector", Path: "/connectors", - Repository: func(q resource.Queryer) resource.Repository { return connector.New(q) }, - Fields: []string{"name", "description", "url", "authorization_mode", "authorization_method", "oauth_config", "oauth_client_secret", "auth_header_name", "proxy_id", "enabled", "config_revision", "connection_status"}, - UserFields: []string{"name", "description", "url", "authorization_mode", "authorization_method", "oauth_config", "oauth_client_secret", "tag_ids"}, - OverrideFields: []string{"name", "description", "url", "proxy_id", "authorization_mode", "authorization_method", "oauth_config", "oauth_client_secret", "auth_header_name", "tag_ids"}, - Hidden: []string{"oauth_client_secret", "icon_s3_key"}, + Repository: func(q resource.Queryer) resource.Repository { return connector.New(q) }, + Fields: []string{"name", "description", "url", "authorization_mode", "authorization_method", "oauth_config", "oauth_client_secret", "auth_header_name", "proxy_id", "enabled", "config_revision", "connection_status"}, + UserFields: []string{"name", "description", "url", "authorization_mode", "authorization_method", "oauth_config", "oauth_client_secret", "tag_ids"}, + OverrideFields: []string{"name", "description", "url", "proxy_id", "authorization_mode", "authorization_method", "oauth_config", "oauth_client_secret", "auth_header_name", "tag_ids"}, + Hidden: []string{"oauth_client_secret", "icon_s3_key"}, PrepareOverride: s.validateConnector, Validate: s.validateConnector, Decorate: s.decorateConnector, UserDecorate: userIcon, References: func(ctx context.Context, tx pgx.Tx, id string) ([]resource.Object, error) { @@ -79,6 +80,7 @@ func (s *Service) validateConnector(ctx context.Context, tx pgx.Tx, in, old reso return resource.Invalid("系统代理不可用") } if _, err := s.proxies.ProxyURL(ctx, id); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "validateConnector", "error", err) return resource.Invalid("系统代理不存在") } } @@ -281,6 +283,9 @@ func (s *Service) updateTool(w http.ResponseWriter, r *http.Request) { Credits billing.Amount `json:"credits_per_call"` } if err := resource.Decode(w, r, &in); err != nil || in.Enabled == nil || in.Credits < 0 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "mcp", "operation", "updateTool", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("工具配置无效")) return } diff --git a/monkeyai/backend/internal/mcp/tools.go b/monkeyai/backend/internal/mcp/tools.go index f969590a0..e31a79a55 100644 --- a/monkeyai/backend/internal/mcp/tools.go +++ b/monkeyai/backend/internal/mcp/tools.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" @@ -177,6 +178,7 @@ func (s *Service) testConnection(ctx context.Context, c, cred resource.Object, u _, err = queries.InvalidateTools(ctx, sqlc.InvalidateToolsParams{ConnectorID: c.String("id"), CredentialID: cred.String("id")}) for _, tool := range tools { if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "testConnection", "error", err) break } schema, marshalErr := json.Marshal(tool.InputSchema) @@ -206,6 +208,7 @@ func (s *Service) testConnection(ctx context.Context, c, cred resource.Object, u return nil, err } if discoveryErr != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "testConnection", "error", discoveryErr) return nil, &resource.Error{Status: 502, Code: "upstream_error", Message: message} } return resource.Object{"credential_id": cred.String("id"), "connection_status": status, "tool_count": len(tools)}, nil diff --git a/monkeyai/backend/internal/mcp/transport.go b/monkeyai/backend/internal/mcp/transport.go index 9c3c233b5..3023b29cc 100644 --- a/monkeyai/backend/internal/mcp/transport.go +++ b/monkeyai/backend/internal/mcp/transport.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "log/slog" "net" "net/http" "net/netip" @@ -150,6 +151,7 @@ func discoverWithClient(ctx context.Context, target string, headers map[string]s NextCursor string `json:"nextCursor"` } if err = json.Unmarshal(data, &list); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "mcp", "operation", "discoverWithClient", "error", err) return nil, fmt.Errorf("工具目录格式无效") } for _, t := range list.Tools { diff --git a/monkeyai/backend/internal/model/admin.go b/monkeyai/backend/internal/model/admin.go index 2463d890e..30d8f8002 100644 --- a/monkeyai/backend/internal/model/admin.go +++ b/monkeyai/backend/internal/model/admin.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/rootgroup" "github.com/go-chi/chi/v5" @@ -54,6 +55,7 @@ func (s *Service) RegisterAdmin(router chi.Router) { func (s *Service) createModel(w http.ResponseWriter, r *http.Request) { var input SaveInput if err := decodeModelRequest(w, r, &input); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "model", "operation", "createModel", "error", err) modelError(w, http.StatusBadRequest, "请求格式无效") return } @@ -69,6 +71,7 @@ func (s *Service) createModel(w http.ResponseWriter, r *http.Request) { func (s *Service) updateModel(w http.ResponseWriter, r *http.Request) { var input SaveInput if err := decodeModelRequest(w, r, &input); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "model", "operation", "updateModel", "error", err) modelError(w, http.StatusBadRequest, "请求格式无效") return } @@ -92,6 +95,9 @@ func (s *Service) setModelEnabled(w http.ResponseWriter, r *http.Request) { Enabled *bool `json:"enabled"` } if err := decodeModelRequest(w, r, &input); err != nil || input.Enabled == nil { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "model", "operation", "setModelEnabled", "error", err) + } modelError(w, http.StatusBadRequest, "enabled 必须是布尔值") return } diff --git a/monkeyai/backend/internal/model/image.go b/monkeyai/backend/internal/model/image.go index 61ca90c57..e4672fa1e 100644 --- a/monkeyai/backend/internal/model/image.go +++ b/monkeyai/backend/internal/model/image.go @@ -3,6 +3,7 @@ package model import ( "encoding/json" "errors" + "log/slog" "slices" "strconv" "strings" @@ -56,6 +57,9 @@ func validateImageModel(item *Model) error { if len(item.ProviderOptions) != 0 { var options map[string]json.RawMessage if err := json.Unmarshal(item.ProviderOptions, &options); err != nil || options == nil { + if err != nil { + slog.Warn("操作失败", "module", "model", "operation", "validateImageModel", "error", err) + } return errors.New("provider_options 必须是 JSON 对象") } } @@ -112,6 +116,9 @@ func validRatio(value string) bool { func validateImagePricing(config *ImageConfig, pricing *ImagePricing) error { base, err := billing.ParseAmount(pricing.BaseCreditsPerImage) if err != nil || base < 0 { + if err != nil { + slog.Warn("操作失败", "module", "model", "operation", "validateImagePricing", "error", err) + } return errors.New("base_credits_per_image 必须是非负积分") } seen := make(map[string]bool, len(pricing.QualityMultipliers)) @@ -122,6 +129,9 @@ func validateImagePricing(config *ImageConfig, pricing *ImagePricing) error { seen[entry.Name] = true value, err := billing.ParseAmount(entry.Multiplier) if err != nil || value <= 0 { + if err != nil { + slog.Warn("操作失败", "module", "model", "operation", "validateImagePricing", "error", err) + } return errors.New("quality 倍率必须大于 0") } } diff --git a/monkeyai/backend/internal/model/page.go b/monkeyai/backend/internal/model/page.go index 44bce8111..2cc4cdbf3 100644 --- a/monkeyai/backend/internal/model/page.go +++ b/monkeyai/backend/internal/model/page.go @@ -4,11 +4,12 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "strconv" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/database" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/model/sqlc" ) @@ -78,6 +79,9 @@ func (s *Service) listModels(w http.ResponseWriter, r *http.Request) { if raw := query.Get("limit"); raw != "" { limit, err = strconv.Atoi(raw) if err != nil || limit < 1 || limit > 500 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "model", "operation", "listModels", "error", err) + } modelError(w, http.StatusBadRequest, "limit 必须在 1—500 之间") return } diff --git a/monkeyai/backend/internal/model/service.go b/monkeyai/backend/internal/model/service.go index 2b345c200..79e8ae419 100644 --- a/monkeyai/backend/internal/model/service.go +++ b/monkeyai/backend/internal/model/service.go @@ -3,12 +3,13 @@ package model import ( "context" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/url" "slices" "strings" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" ) @@ -222,6 +223,9 @@ func (s *Service) AgentModels(ctx context.Context, userID string, isAdmin bool) if item.Kind == KindVideo && item.VideoConfig != nil { cap, err := VideoCapabilitiesFor(item.Provider, item.ModelID) if err != nil || ValidateVideoCapabilities(item, cap) != nil { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "model", "operation", "AgentModels", "error", err) + } continue } videoConfig = &AgentVideoConfig{VideoConfig: *item.VideoConfig, @@ -272,6 +276,7 @@ func (s *Service) Resolve(ctx context.Context, credential, requestedModel string } userID, err := s.keys.Authenticate(ctx, credential, "model:invoke") if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "model", "operation", "Resolve", "error", err) return Target{}, ErrUnauthorized } item, err := s.repository.Resolve(ctx, userID, requestedModel) @@ -354,6 +359,9 @@ func modelFromInput(input SaveInput) (Model, error) { } parsed, err := url.Parse(item.BaseURL) if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Hostname() == "" || parsed.User != nil { + if err != nil { + slog.Warn("操作失败", "module", "model", "operation", "modelFromInput", "error", err) + } return Model{}, errors.New("base_url 必须是有效的 HTTP(S) 地址") } if kind == KindVideo && (parsed.RawQuery != "" || parsed.Fragment != "") { diff --git a/monkeyai/backend/internal/model/video.go b/monkeyai/backend/internal/model/video.go index 812baf69e..2bba943e1 100644 --- a/monkeyai/backend/internal/model/video.go +++ b/monkeyai/backend/internal/model/video.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "slices" "strconv" "strings" @@ -33,7 +34,11 @@ func marshalVideoFields(item Model) ([]byte, []byte, error) { func videoValues(values ...any) []json.RawMessage { result := make([]json.RawMessage, 0, len(values)) for _, value := range values { - data, _ := json.Marshal(value) + data, err := json.Marshal(value) + if err != nil { + slog.Error("编码视频能力失败", "error", err) + continue + } result = append(result, data) } return result @@ -179,6 +184,7 @@ func userVideoPricing(kind Kind, provider Provider, modelID string, config *Vide } cap, err := VideoCapabilitiesFor(provider, modelID) if err != nil { + slog.Warn("操作失败", "module", "model", "operation", "userVideoPricing", "error", err) return nil } pricing := &VideoPricing{Rates: []VideoResolutionRate{}} @@ -290,6 +296,9 @@ func ValidateVideoCapabilities(item Model, cap VideoCapabilities) error { for _, entry := range item.VideoPricing.Rates { amount, err := billing.ParseAmount(entry.CreditsPerSecond) if !resolutions[entry.Resolution] || err != nil || amount < 0 { + if err != nil { + slog.Warn("操作失败", "module", "model", "operation", "ValidateVideoCapabilities", "error", err) + } return errors.New("视频清晰度或每秒积分无效") } delete(resolutions, entry.Resolution) @@ -428,12 +437,22 @@ func VideoMaxDuration(cap VideoCapabilities) (int64, error) { func VideoDuration(values map[string]json.RawMessage) (int64, error) { var seconds int64 if err := json.Unmarshal(values["duration_seconds"], &seconds); err != nil || seconds < 1 || seconds > billing.MaxVideoDurationMs/1000 { + if err != nil { + slog.Warn("操作失败", "module", "model", "operation", "VideoDuration", "error", err) + } return 0, errors.New("视频时长无效") } return seconds, nil } func VideoResolution(values map[string]json.RawMessage) string { - value, _ := strconv.Unquote(strings.TrimSpace(string(values["resolution"]))) + if len(values["resolution"]) == 0 { + return "" + } + value, err := strconv.Unquote(strings.TrimSpace(string(values["resolution"]))) + if err != nil { + slog.Error("解析视频分辨率失败", "error", err) + return "" + } return value } diff --git a/monkeyai/backend/internal/proxy/billing.go b/monkeyai/backend/internal/proxy/billing.go index 3cc666396..34a327eea 100644 --- a/monkeyai/backend/internal/proxy/billing.go +++ b/monkeyai/backend/internal/proxy/billing.go @@ -4,11 +4,12 @@ import ( "context" "encoding/json" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" ) @@ -96,6 +97,9 @@ func prepareRequest(body []byte, path string, limit int64, stream bool) ([]byte, options := map[string]json.RawMessage{} if b, ok := data["stream_options"]; ok { if err := json.Unmarshal(b, &options); err != nil || options == nil { + if err != nil { + slog.Warn("操作失败", "module", "proxy", "operation", "prepareRequest", "error", err) + } return nil, resource.Invalid("stream_options 无效") } } diff --git a/monkeyai/backend/internal/proxy/encoding.go b/monkeyai/backend/internal/proxy/encoding.go index 087436380..2c5cffe0c 100644 --- a/monkeyai/backend/internal/proxy/encoding.go +++ b/monkeyai/backend/internal/proxy/encoding.go @@ -1,6 +1,7 @@ package proxy import ( + "log/slog" "strconv" "strings" ) @@ -28,6 +29,9 @@ func negotiateEncoding(values []string) (string, bool) { } q, err := strconv.ParseFloat(strings.TrimSpace(weight), 64) if err != nil || !(q >= 0 && q <= 1) { + if err != nil { + slog.Warn("操作失败", "module", "proxy", "operation", "negotiateEncoding", "error", err) + } quality = 0 break } diff --git a/monkeyai/backend/internal/proxy/estimate.go b/monkeyai/backend/internal/proxy/estimate.go index c55c565c5..1fd76de33 100644 --- a/monkeyai/backend/internal/proxy/estimate.go +++ b/monkeyai/backend/internal/proxy/estimate.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "log/slog" "strings" "sync" "unicode/utf8" @@ -15,11 +16,19 @@ const maxEstimateBytes = 32 << 20 var ( o200kCodec = sync.OnceValue(func() tokenizer.Codec { - codec, _ := tokenizer.Get(tokenizer.O200kBase) + codec, err := tokenizer.Get(tokenizer.O200kBase) + if err != nil { + slog.Error("加载分词器失败", "error", err) + return nil + } return codec }) cl100kCodec = sync.OnceValue(func() tokenizer.Codec { - codec, _ := tokenizer.Get(tokenizer.Cl100kBase) + codec, err := tokenizer.Get(tokenizer.Cl100kBase) + if err != nil { + slog.Error("加载分词器失败", "error", err) + return nil + } return codec }) ) @@ -44,6 +53,7 @@ func newTokenEstimate(pc *proxyContext) *tokenEstimate { var err error e.request, err = estimateObject(pc.requestBody) if err != nil { + slog.Warn("操作失败", "module", "proxy", "operation", "newTokenEstimate", "error", err) return nil } model := pc.target.UpstreamModel @@ -167,6 +177,7 @@ func (e *tokenEstimate) jsonPart(key string, value any) { } data, err := json.Marshal(value) if err != nil { + slog.Warn("操作失败", "module", "proxy", "operation", "jsonPart", "error", err) e.supported = false return } @@ -256,6 +267,9 @@ func (e *tokenEstimate) response(path string, data []byte) { } response, err := estimateObject(data) if err != nil || response == nil { + if err != nil { + slog.Warn("操作失败", "module", "proxy", "operation", "response", "error", err) + } e.supported = false return } @@ -300,6 +314,9 @@ func (e *tokenEstimate) event(path string, event sseEvent) { } data, err := estimateObject(event.Data) if err != nil || data == nil { + if err != nil { + slog.Warn("操作失败", "module", "proxy", "operation", "event", "error", err) + } e.supported = false return } diff --git a/monkeyai/backend/internal/proxy/observation.go b/monkeyai/backend/internal/proxy/observation.go index c88f84b77..973cf3d29 100644 --- a/monkeyai/backend/internal/proxy/observation.go +++ b/monkeyai/backend/internal/proxy/observation.go @@ -27,7 +27,6 @@ func (p *Proxy) logCall(ctx context.Context, pc *proxyContext, call Call, cause if ctx.Err() != nil && result == "failed" && (call.ErrorCode == "stream_interrupted" || call.ErrorCode == "upstream_connection_failed") { result, level = "canceled", slog.LevelDebug } - logging.CountCall(p.logger, result) attrs := []slog.Attr{slog.String("module", "proxy"), slog.String("operation", "proxy_request"), slog.String("model_id", pc.target.ModelID), slog.String("transaction_id", pc.reservation.ID), slog.String("response_id", call.RequestID), slog.String("result", result), slog.Bool("stream", pc.stream), diff --git a/monkeyai/backend/internal/proxy/observation_test.go b/monkeyai/backend/internal/proxy/observation_test.go index dd41038d0..369e42b68 100644 --- a/monkeyai/backend/internal/proxy/observation_test.go +++ b/monkeyai/backend/internal/proxy/observation_test.go @@ -32,11 +32,10 @@ func TestModelFailureObservedWithoutBillingOrRecorder(t *testing.T) { })) defer upstream.Close() var output bytes.Buffer - logs, _ := logging.NewRuntime(&output, slog.LevelDebug, logging.DefaultSettings()) - defer logs.Close() + logger := logging.New(&output, slog.LevelDebug) p := NewProxy(ResolverFunc(func(context.Context, string, string) (Target, error) { return Target{ModelID: "model-1", BaseURL: upstream.URL}, nil - }), logs.Logger()) + }), logger) body := `{"model":"test","messages":[],"stream":false}` if test.stream { body = `{"model":"test","messages":[],"stream":true}` @@ -44,7 +43,7 @@ func TestModelFailureObservedWithoutBillingOrRecorder(t *testing.T) { r := httptest.NewRequest("POST", "/v1/chat/completions", strings.NewReader(body)) r.Header.Set("Authorization", "Bearer private-key") response := httptest.NewRecorder() - logging.HTTP(logs.Logger())(p).ServeHTTP(response, r) + logging.HTTP(logger)(p).ServeHTTP(response, r) if err := p.Wait(t.Context()); err != nil { t.Fatal(err) } diff --git a/monkeyai/backend/internal/proxy/proxy.go b/monkeyai/backend/internal/proxy/proxy.go index f407eab56..8c4f853f7 100644 --- a/monkeyai/backend/internal/proxy/proxy.go +++ b/monkeyai/backend/internal/proxy/proxy.go @@ -166,11 +166,13 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 32<<20)) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "proxy", "operation", "ServeHTTP", "error", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } meta, err := readRequestMeta(body) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "proxy", "operation", "ServeHTTP", "error", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } @@ -207,6 +209,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { if target.UpstreamModel != "" && target.UpstreamModel != meta.Model { body, err = rewriteModel(body, target.UpstreamModel) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "proxy", "operation", "ServeHTTP", "error", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } @@ -281,6 +284,9 @@ func protocolForPath(requestPath string) string { func rewriteModel(body []byte, upstreamModel string) ([]byte, error) { var request map[string]json.RawMessage if err := json.Unmarshal(body, &request); err != nil || request == nil { + if err != nil { + slog.Warn("操作失败", "module", "proxy", "operation", "rewriteModel", "error", err) + } return nil, errors.New("请求体必须是 JSON 对象") } encoded, err := json.Marshal(upstreamModel) diff --git a/monkeyai/backend/internal/proxy/proxy_test.go b/monkeyai/backend/internal/proxy/proxy_test.go index 617428b34..fcfa151b9 100644 --- a/monkeyai/backend/internal/proxy/proxy_test.go +++ b/monkeyai/backend/internal/proxy/proxy_test.go @@ -304,7 +304,7 @@ func (r *usageRecorderStub) Record(_ context.Context, call Call) error { return nil } -func TestProxyErrorHandlerRedactsUpstreamError(t *testing.T) { +func TestProxyErrorHandlerLogsUpstreamError(t *testing.T) { var logs bytes.Buffer proxy := NewProxy(nil, slog.New(slog.NewTextHandler(&logs, nil))) pc := &proxyContext{target: Target{ModelID: "model-1"}, reservation: Reservation{ID: "transaction-1"}} @@ -316,7 +316,7 @@ func TestProxyErrorHandlerRedactsUpstreamError(t *testing.T) { t.Fatalf("代理错误响应不正确: %d %q", recorder.Code, recorder.Body.String()) } text := logs.String() - if !strings.Contains(text, "model-1") || !strings.Contains(text, "transaction-1") || !strings.Contains(text, "proxy_request") || strings.Contains(text, "upstream-token-and-private-response") { - t.Fatalf("代理日志缺少安全上下文或泄漏上游详情: %s", text) + if !strings.Contains(text, "model-1") || !strings.Contains(text, "transaction-1") || !strings.Contains(text, "proxy_request") || !strings.Contains(text, "upstream-token-and-private-response") { + t.Fatalf("代理日志缺少上下文或原始错误: %s", text) } } diff --git a/monkeyai/backend/internal/proxy/reconcile.go b/monkeyai/backend/internal/proxy/reconcile.go index dfd9d60c1..cf89ef7ff 100644 --- a/monkeyai/backend/internal/proxy/reconcile.go +++ b/monkeyai/backend/internal/proxy/reconcile.go @@ -4,11 +4,12 @@ import ( "context" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net/http" "strings" + + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" ) type ResponseReconciliationState string @@ -77,8 +78,7 @@ func (r *ResponseReconciler) Reconcile(ctx context.Context, target Target, respo } defer func() { if closeErr := response.Body.Close(); closeErr != nil && ctx.Err() == nil && !errors.Is(closeErr, context.Canceled) && !errors.Is(closeErr, io.ErrClosedPipe) { - // 自定义 Transport 的关闭错误可能包含凭据或响应内容。 - logging.Event(ctx, slog.Default(), slog.LevelWarn, "proxy.operation.warn", "关闭上游对账响应失败", "module", "proxy", "operation", "Reconcile", "model_id", target.ModelID, "operation", "close_reconciliation_response", "error_type", fmt.Sprintf("%T", closeErr)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "proxy.operation.warn", "关闭上游对账响应失败", "module", "proxy", "operation", "Reconcile", "model_id", target.ModelID, "operation", "close_reconciliation_response", "error_type", fmt.Sprintf("%T", closeErr), "error", closeErr) } }() diff --git a/monkeyai/backend/internal/proxy/reconcile_test.go b/monkeyai/backend/internal/proxy/reconcile_test.go index 81c35385c..9fdbde13c 100644 --- a/monkeyai/backend/internal/proxy/reconcile_test.go +++ b/monkeyai/backend/internal/proxy/reconcile_test.go @@ -103,7 +103,7 @@ func TestResponseReconciliationCloseLogIsSafe(t *testing.T) { t.Fatalf("响应关闭不能改变对账结果: %+v %v", result, err) } text := logs.String() - if strings.Contains(text, "private-upstream-key-and-body") || tc.logged && (!strings.Contains(text, "close_reconciliation_response") || !strings.Contains(text, "model-1")) || !tc.logged && text != "" { + if tc.logged && !strings.Contains(text, tc.closeErr.Error()) || tc.logged && (!strings.Contains(text, "close_reconciliation_response") || !strings.Contains(text, "model-1")) || !tc.logged && text != "" { t.Fatalf("关闭响应时的日志不安全或有误报: %s", text) } }) diff --git a/monkeyai/backend/internal/proxy/transport.go b/monkeyai/backend/internal/proxy/transport.go index 83584aded..ecfb6c85c 100644 --- a/monkeyai/backend/internal/proxy/transport.go +++ b/monkeyai/backend/internal/proxy/transport.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + "log/slog" "net" "net/http" "net/url" @@ -26,6 +27,9 @@ func (e *ConnectError) DiagnosticCode() string { func TransportFor(raw string) (*http.Transport, error) { address, err := url.Parse(raw) if err != nil || address.Host == "" { + if err != nil { + slog.Warn("操作失败", "module", "proxy", "operation", "TransportFor", "error", err) + } return nil, fmt.Errorf("系统代理配置无效") } transport := DirectTransport() diff --git a/monkeyai/backend/internal/proxy/usage_test.go b/monkeyai/backend/internal/proxy/usage_test.go index df6ba9a8d..13903fa76 100644 --- a/monkeyai/backend/internal/proxy/usage_test.go +++ b/monkeyai/backend/internal/proxy/usage_test.go @@ -208,7 +208,7 @@ func TestUsageCapturePipeErrorLogsDetailWithoutResponse(t *testing.T) { t.Fatal(err) } text := logs.String() - if strings.Count(text, "write_usage_pipe") != 1 || !strings.Contains(text, "error.error_code=internal_error") || strings.Contains(text, "用量解析器意外失败") || !strings.Contains(text, "transaction-1") || !strings.Contains(text, "model-1") || strings.Contains(text, "private-upstream-response") { + if strings.Count(text, "write_usage_pipe") != 1 || !strings.Contains(text, "error.error_code=internal_error") || !strings.Contains(text, "用量解析器意外失败") || !strings.Contains(text, "transaction-1") || !strings.Contains(text, "model-1") || strings.Contains(text, "private-upstream-response") { t.Fatalf("管道错误日志缺少详情或泄漏响应: %s", text) } } diff --git a/monkeyai/backend/internal/relay/bridge.go b/monkeyai/backend/internal/relay/bridge.go index e11ab75d3..d698b141e 100644 --- a/monkeyai/backend/internal/relay/bridge.go +++ b/monkeyai/backend/internal/relay/bridge.go @@ -7,12 +7,13 @@ import ( "encoding/base64" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "time" "uuid" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/endpoint" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" @@ -59,18 +60,23 @@ func (s *Service) bridge(w http.ResponseWriter, r *http.Request) { defer func() { s.mu.Lock(); s.slots--; s.mu.Unlock() }() ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{InsecureSkipVerify: true, CompressionMode: websocket.CompressionDisabled}) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "bridge", "error", err) return } s.mu.Lock() if s.draining.Load() { s.mu.Unlock() - ws.CloseNow() + if err := ws.CloseNow(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "bridge", "error", err) + } return } s.active[ws] = struct{}{} s.mu.Unlock() defer func() { - ws.CloseNow() + if err := ws.CloseNow(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "bridge", "error", err) + } s.mu.Lock() delete(s.active, ws) s.mu.Unlock() @@ -106,6 +112,7 @@ func (s *Service) bridge(w http.ResponseWriter, r *http.Request) { defer cancel() nonce, err := randomSecret() if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "bridge", "error", err) return } challenge := Challenge{Type: "challenge", ServerID: s.config.ServerID, OwnerUserID: p.UserID, ChallengeID: uuid.New().String(), Nonce: nonce, TTLMS: 60000, ServerTime: time.Now().UTC(), GrantID: p.GrantID, RegistrationVersion: state.RegistrationVersion} @@ -114,12 +121,22 @@ func (s *Service) bridge(w http.ResponseWriter, r *http.Request) { } var proof Proof if err := readWS(handshake, ws, &proof); err != nil || proof.Type != "proof" || proof.ChallengeID != challenge.ChallengeID { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "bridge", "error", err) + } s.wsError(handshake, ws, errProtocol) return } signature, err := base64.RawURLEncoding.Strict().DecodeString(proof.Signature) - message, _ := ProofBytes(hello, challenge) + message, encodeErr := ProofBytes(hello, challenge) + if encodeErr != nil { + s.wsError(handshake, ws, encodeErr) + return + } if err != nil || len(public) != ed25519.PublicKeySize || !ed25519.Verify(public, message, signature) { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "bridge", "error", err) + } s.wsError(handshake, ws, &resource.Error{Status: 403, Code: "invalid_device_proof", Message: "设备证明无效"}) return } @@ -188,6 +205,9 @@ func (s *Service) bridge(w http.ResponseWriter, r *http.Request) { } var ack ReadyAck if err := readWS(handshake, ws, &ack); err != nil || ack.Type != "ready_ack" || ack.RegistrationVersion != ready.RegistrationVersion || !identityOnly && ack.LeaseEpoch != ready.LeaseEpoch { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "bridge", "error", err) + } s.wsError(handshake, ws, errProtocol) return } @@ -199,9 +219,14 @@ func (s *Service) bridge(w http.ResponseWriter, r *http.Request) { ws.SetReadLimit(1 << 20) mux, err := yamux.Server(conn, muxConfig()) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "bridge", "error", err) return } - defer mux.Close() + defer func() { + if err := mux.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "bridge", "error", err) + } + }() if channel == "data" { gate.Lock() s.mu.Lock() @@ -217,7 +242,9 @@ func (s *Service) bridge(w http.ResponseWriter, r *http.Request) { gate.Unlock() logging.Event(r.Context(), s.logger, slog.LevelInfo, "relay.bridge.ready", "设备数据通道已就绪") if previous != nil { - previous.Close() + if err := previous.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "bridge", "error", err) + } } defer func() { s.mu.Lock() @@ -228,7 +255,9 @@ func (s *Service) bridge(w http.ResponseWriter, r *http.Request) { }() // 数据通道只接受控制面打开的业务流,拒绝执行端反向拨号。 if incoming, err := mux.AcceptStream(); err == nil { - incoming.Close() + if err := incoming.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "bridge", "error", err) + } } return } @@ -265,7 +294,9 @@ func (s *Service) wsError(ctx context.Context, ws *websocket.Conn, err error) { } else if errors.Is(err, errProtocol) { code = "invalid_tunnel_message" } - _ = writeWS(ctx, ws, map[string]any{"type": "error", "code": code}) + if err := writeWS(ctx, ws, map[string]any{"type": "error", "code": code}); err != nil { + slog.Error("操作失败", "module", "relay", "operation", "wsError", "error", err) + } logCode := "internal_error" switch code { case "authorization_state_unavailable", "invalid_tunnel_message", "permission_denied", "invalid_device_proof", "device_process_conflict", "device_registration_changed", "device_revoked", "device_not_found": diff --git a/monkeyai/backend/internal/relay/connection.go b/monkeyai/backend/internal/relay/connection.go index 9e20f3b80..90a304ffe 100644 --- a/monkeyai/backend/internal/relay/connection.go +++ b/monkeyai/backend/internal/relay/connection.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "log/slog" "net/http" "slices" "time" @@ -55,6 +56,9 @@ func (s *Service) connections(w http.ResponseWriter, r *http.Request) { var in ConnectionRequest data, err := readBody(w, r, maxControlMessage) if err != nil || decodeMessage(data, &in) != nil { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "connections", "error", err) + } failure(r.Context(), w, 400, "invalid_request") return } @@ -82,22 +86,30 @@ func (s *Service) connections(w http.ResponseWriter, r *http.Request) { } ticket, err := randomSecret() if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "connections", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } tx, err := s.pool.Begin(r.Context()) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "connections", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } - defer tx.Rollback(context.WithoutCancel(r.Context())) + defer func() { + if err := tx.Rollback(context.WithoutCancel(r.Context())); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "relay", "operation", "connections", "error", err) + } + }() q := sqlc.New(tx) if _, err = q.LockUser(r.Context(), p.UserID); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "connections", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } valid, err := s.identity.AuthorizeDevice(r.Context(), tx, p, "device:connect") if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "connections", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } @@ -116,6 +128,7 @@ func (s *Service) connections(w http.ResponseWriter, r *http.Request) { } hostValid, hostErr := s.identity.AuthorizeDevice(r.Context(), tx, route.principal, "device:publish") if hostErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "connections", "error", hostErr) failure(r.Context(), w, 503, "authorization_state_unavailable") return } @@ -135,6 +148,9 @@ func (s *Service) connections(w http.ResponseWriter, r *http.Request) { row, err = q.LockConnection(r.Context(), sqlc.LockConnectionParams{ID: in.ConnectionID, UserID: p.UserID, ClientGrantID: p.GrantID}) if err != nil || row.MachineID != machine || row.RevokedAt != nil || !time.Now().Before(row.ExpiresAt) || row.GrantVersion != state.GrantVersion { if err != nil && !errors.Is(err, pgx.ErrNoRows) { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "connections", "error", err) + } failure(r.Context(), w, 503, "authorization_state_unavailable") } else { failure(r.Context(), w, 401, "connection_expired") @@ -168,6 +184,7 @@ func (s *Service) connections(w http.ResponseWriter, r *http.Request) { } count, err := q.CountConnections(r.Context(), sqlc.CountConnectionsParams{UserID: p.UserID, MachineID: machine, GrantVersion: state.GrantVersion}) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "connections", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } @@ -194,7 +211,13 @@ func (s *Service) connections(w http.ResponseWriter, r *http.Request) { } negotiated[name] = 1 } - protocol, _ = json.Marshal(negotiated) + var encodeErr error + protocol, encodeErr = json.Marshal(negotiated) + if encodeErr != nil { + s.logger.ErrorContext(r.Context(), "编码连接协议失败", "error", encodeErr) + failure(r.Context(), w, 500, "internal_error") + return + } if in.ConnectionID != "" && !sameProtocol(row.Protocol, protocol) { failure(r.Context(), w, 409, "upgrade_required") return @@ -207,10 +230,12 @@ func (s *Service) connections(w http.ResponseWriter, r *http.Request) { row, err = q.RotateTicket(r.Context(), sqlc.RotateTicketParams{ID: row.ID, TicketHash: secretHash(ticket), Scopes: scopes, ExpiresAt: expiry}) } if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "connections", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } if err = tx.Commit(r.Context()); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "connections", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } diff --git a/monkeyai/backend/internal/relay/directory.go b/monkeyai/backend/internal/relay/directory.go index 8adf0a3d2..905375a10 100644 --- a/monkeyai/backend/internal/relay/directory.go +++ b/monkeyai/backend/internal/relay/directory.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "log/slog" "net/http" "strconv" "time" @@ -54,6 +55,7 @@ func (s *Service) directory(w http.ResponseWriter, r *http.Request) { if value := r.URL.Query().Get("page"); value != "" { page, err = strconv.Atoi(value) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "directory", "error", err) failure(r.Context(), w, 400, "invalid_request") return } @@ -61,6 +63,7 @@ func (s *Service) directory(w http.ResponseWriter, r *http.Request) { if value := r.URL.Query().Get("page_size"); value != "" { size, err = strconv.Atoi(value) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "directory", "error", err) failure(r.Context(), w, 400, "invalid_request") return } @@ -72,6 +75,7 @@ func (s *Service) directory(w http.ResponseWriter, r *http.Request) { } rows, err := s.devices.Snapshot(ctx, p.UserID, capability == "execute") if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "directory", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } @@ -87,6 +91,7 @@ func (s *Service) directory(w http.ResponseWriter, r *http.Request) { } else if online { valid, err := s.identity.VerifyGrant(ctx, current.principal) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "directory", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } @@ -115,6 +120,7 @@ func (s *Service) directory(w http.ResponseWriter, r *http.Request) { } encoded, err := json.Marshal(out) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "directory", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } diff --git a/monkeyai/backend/internal/relay/file.go b/monkeyai/backend/internal/relay/file.go index ab14c3264..ea74f3ddb 100644 --- a/monkeyai/backend/internal/relay/file.go +++ b/monkeyai/backend/internal/relay/file.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "log/slog" "net/http" "net/url" "slices" @@ -14,6 +15,7 @@ import ( "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/relay/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "github.com/go-chi/chi/v5" ) @@ -81,6 +83,9 @@ func (s *Service) verifyFileV1(ctx context.Context, machine, raw, requested stri payload, err := base64.RawURLEncoding.Strict().DecodeString(parts[1]) var in FileGrantV1 if err != nil || len(payload) > 8<<10 || decodeMessage(payload, &in) != nil || in.Type != "monkeyai.file-grant.v1" || in.Version != 1 || in.KeyID == "" || in.SessionID == "" || in.Path == "" || (in.Purpose != "resource" && in.Purpose != "download") { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "relay", "operation", "verifyFileV1", "error", err) + } return FileGrant{}, nil, denied } if !safePath("/"+in.Path) || requested != "/"+in.Path { @@ -107,8 +112,14 @@ func (s *Service) verifyFileV1(ctx context.Context, machine, raw, requested stri return FileGrant{}, nil, err } signature, err := base64.RawURLEncoding.Strict().DecodeString(parts[2]) - message, _ := FileGrantV1Bytes(in) + message, encodeErr := FileGrantV1Bytes(in) + if encodeErr != nil { + return FileGrant{}, nil, encodeErr + } if err != nil || len(state.PublicKey) != ed25519.PublicKeySize || in.KeyID != state.KeyID || !ed25519.Verify(state.PublicKey, message, signature) { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "relay", "operation", "verifyFileV1", "error", err) + } return FileGrant{}, nil, denied } now := time.Now() @@ -152,6 +163,9 @@ func (s *Service) verifyFile(ctx context.Context, machine, raw, requested string payload, err := base64.RawURLEncoding.Strict().DecodeString(parts[0]) var grant FileGrant if err != nil || decodeMessage(payload, &grant) != nil || grant.Version != 1 || grant.ServerID != s.config.ServerID || grant.MachineID != machine || !validUUID(grant.OwnerUserID) || !validUUID(grant.SessionID) { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "relay", "operation", "verifyFile", "error", err) + } return grant, nil, denied } state, err := s.devices.State(ctx, grant.OwnerUserID, machine) @@ -163,8 +177,14 @@ func (s *Service) verifyFile(ctx context.Context, machine, raw, requested string return grant, nil, err } signature, err := base64.RawURLEncoding.Strict().DecodeString(parts[1]) - message, _ := FileGrantBytes(grant) + message, encodeErr := FileGrantBytes(grant) + if encodeErr != nil { + return grant, nil, encodeErr + } if err != nil || len(state.PublicKey) != ed25519.PublicKeySize || grant.KeyID != state.KeyID || !ed25519.Verify(state.PublicKey, message, signature) { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "relay", "operation", "verifyFile", "error", err) + } return grant, nil, denied } now := time.Now() @@ -189,7 +209,12 @@ func (s *Service) verifyFile(ctx context.Context, machine, raw, requested string } func (s *Service) file(w http.ResponseWriter, r *http.Request) { - origin, _ := url.Parse(s.config.FilesOrigin) + origin, err := url.Parse(s.config.FilesOrigin) + if err != nil { + logging.Diagnostic(r.Context(), s.logger, slog.LevelError, "relay.file.failed", err) + failure(r.Context(), w, 500, "internal_error") + return + } if !strings.EqualFold(r.Host, origin.Host) || r.Method != "GET" && r.Method != "HEAD" { failure(r.Context(), w, 403, "permission_denied") return @@ -197,6 +222,9 @@ func (s *Service) file(w http.ResponseWriter, r *http.Request) { machine, raw := chi.URLParam(r, "machine"), chi.URLParam(r, "grant") requested, pathErr := requestPath(r, "/"+chi.URLParam(r, "*")) if pathErr != nil || !safePath(requested) { + if pathErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "file", "error", pathErr) + } failure(r.Context(), w, 400, "invalid_request") return } @@ -214,6 +242,7 @@ func (s *Service) file(w http.ResponseWriter, r *http.Request) { "authorization": map[string]any{"server_id": s.config.ServerID, "owner_user_id": grant.OwnerUserID, "machine_id": machine, "connection_id": grant.ConnectionID, "client_grant_id": grant.ClientGrantID, "grant_version": grant.GrantVersion, "access_version": grant.AccessVersion, "lease_epoch": grant.LeaseEpoch, "scopes": grant.Scopes}, "expires_at": grant.ExpiresAt.UTC().Format(time.RFC3339Nano)}) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "file", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } diff --git a/monkeyai/backend/internal/relay/identity.go b/monkeyai/backend/internal/relay/identity.go index e6c24c60e..c4c89525e 100644 --- a/monkeyai/backend/internal/relay/identity.go +++ b/monkeyai/backend/internal/relay/identity.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "log/slog" "os" "path/filepath" "runtime" @@ -64,8 +65,16 @@ func loadIdentity(dir string) (serverIdentity, error) { if err != nil { return serverIdentity{}, fmt.Errorf("创建远程执行身份文件: %w", err) } - defer os.Remove(temp.Name()) - defer temp.Close() + defer func() { + if err := os.Remove(temp.Name()); err != nil && !os.IsNotExist(err) { + slog.Error("清理临时文件失败", "module", "relay", "operation", "loadIdentity", "error", err) + } + }() + defer func() { + if err := temp.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + slog.Error("关闭资源失败", "module", "relay", "operation", "loadIdentity", "error", err) + } + }() if _, err = temp.Write(data); err != nil { return serverIdentity{}, err } @@ -85,7 +94,9 @@ func loadIdentity(dir string) (serverIdentity, error) { return serverIdentity{}, err } err = directory.Sync() - directory.Close() + if err := directory.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "loadIdentity", "error", err) + } if err != nil { return serverIdentity{}, err } @@ -98,7 +109,11 @@ func readIdentity(path string) (serverIdentity, error) { if err != nil { return serverIdentity{}, err } - defer file.Close() + defer func() { + if err := file.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "readIdentity", "error", err) + } + }() info, err := file.Stat() if err != nil { return serverIdentity{}, err @@ -110,12 +125,16 @@ func readIdentity(path string) (serverIdentity, error) { decoder := json.NewDecoder(io.LimitReader(file, 16<<10)) decoder.DisallowUnknownFields() if err := decoder.Decode(&state); err != nil || !validUUID(state.ServerID) { + if err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "readIdentity", "error", err) + } return serverIdentity{}, errors.New("远程执行身份文件无效,拒绝覆盖已有身份") } if decoder.Decode(new(any)) != io.EOF { return serverIdentity{}, errors.New("远程执行身份文件含多余内容,拒绝覆盖已有身份") } if _, err := parseSigningKey([]byte(state.PrivateKey)); err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "readIdentity", "error", err) return serverIdentity{}, errors.New("远程执行身份私钥无效,拒绝覆盖已有身份") } return state, nil diff --git a/monkeyai/backend/internal/relay/lease.go b/monkeyai/backend/internal/relay/lease.go index 58ad3419c..22846926a 100644 --- a/monkeyai/backend/internal/relay/lease.go +++ b/monkeyai/backend/internal/relay/lease.go @@ -3,7 +3,7 @@ package relay import ( "context" "encoding/json" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "errors" "io" "log/slog" "net" @@ -12,6 +12,8 @@ import ( "sync" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/relay/sqlc" "github.com/hashicorp/yamux" ) @@ -45,7 +47,9 @@ func (a *access) closeReason(reason string) { cancel() } if conn != nil { - conn.Close() + if err := conn.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "closeReason", "error", err) + } } } func (a *access) deadline() time.Time { @@ -64,7 +68,9 @@ func (a *access) renew(verified, expires time.Time) { } a.verifiedAt, a.expiresAt = verified, expires if a.stream != nil { - a.stream.SetDeadline(minTime(expires, verified.Add(180*time.Second))) + if err := a.stream.SetDeadline(minTime(expires, verified.Add(180*time.Second))); err != nil { + slog.Error("设置连接截止时间失败", "module", "relay", "operation", "renew", "error", err) + } } } func minTime(a, b time.Time) time.Time { @@ -122,7 +128,11 @@ func (s *Service) revocation(r *route, connection string, version int64) string payload["connection_id"] = connection payload["access_version"] = version } - wire, _ := signWire(s.key, s.kid, payload) + wire, err := signWire(s.key, s.kid, payload) + if err != nil { + s.logger.Error("签发中转凭据失败", "error", err) + return "" + } return wire } func (s *Service) pushControl(r *route, value any) bool { @@ -264,11 +274,13 @@ func (s *Service) lease(ctx context.Context, r *route, in LeaseRequest) (leaseRe } state := "fresh" if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "relay", "operation", "lease", "error", err) state = "grace" } wire, err := signWire(s.key, s.kid, map[string]any{"type": "monkeyai.access.lease.v1", "issuer": s.config.PublicURL, "audience": "monkeyai-runtime", "kid": s.kid, "authorization": auth, "nonce": in.Nonce, "lease_seq": in.LeaseSeq, "boot_id": in.BootID, "ttl_ms": ttl.Milliseconds(), "ticket_remaining_ms": min(int64(900000), max(0, time.Until(expires).Milliseconds())), "auth_age_ms": age, "server_time": now.UTC().Format(time.RFC3339Nano), "verification_state": state}) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "relay", "operation", "lease", "error", err) return leaseResponse{Code: "authorization_state_unavailable"}, 503 } return leaseResponse{Signed: wire}, 200 @@ -299,7 +311,9 @@ func (l *controlListener) Accept() (net.Conn, error) { case l.slots <- struct{}{}: return &controlConn{Conn: c, slots: l.slots}, nil default: - c.Close() + if err := c.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "Accept", "error", err) + } } } } @@ -322,8 +336,14 @@ func (s *Service) control(ctx context.Context, r *route) { go s.probe(r, stop) server := &http.Server{ErrorLog: logging.ErrorLog(s.logger), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, IdleTimeout: 10 * time.Second, MaxHeaderBytes: 16 << 10, BaseContext: func(net.Listener) context.Context { return ctx }, Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { s.controlHTTP(w, req, r) })} - defer server.Close() - _ = server.Serve(&controlListener{session: r.control, slots: make(chan struct{}, 16)}) + defer func() { + if err := server.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "control", "error", err) + } + }() + if err := server.Serve(&controlListener{session: r.control, slots: make(chan struct{}, 16)}); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Error("操作失败", "module", "relay", "operation", "control", "error", err) + } } func (s *Service) probe(r *route, stop <-chan struct{}) { ticker := time.NewTicker(15 * time.Second) @@ -334,6 +354,7 @@ func (s *Service) probe(r *route, stop <-chan struct{}) { return case <-ticker.C: if _, err := r.control.Ping(); err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "probe", "error", err) s.closeRoute(r) return } @@ -351,7 +372,9 @@ func (s *Service) controlHTTP(w http.ResponseWriter, req *http.Request, r *route return } rc := http.NewResponseController(w) - _ = rc.SetWriteDeadline(time.Now().Add(5 * time.Second)) + if err := rc.SetWriteDeadline(time.Now().Add(5 * time.Second)); err != nil { + slog.Error("设置连接截止时间失败", "module", "relay", "operation", "controlHTTP", "error", err) + } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") switch { @@ -359,19 +382,27 @@ func (s *Service) controlHTTP(w http.ResponseWriter, req *http.Request, r *route s.mu.Lock() online := r.data != nil && !r.data.IsClosed() s.mu.Unlock() - json.NewEncoder(w).Encode(map[string]any{"lease_epoch": r.state.LeaseEpoch, "data_online": online}) + if err := json.NewEncoder(w).Encode(map[string]any{"lease_epoch": r.state.LeaseEpoch, "data_online": online}); err != nil { + slog.Error("写入输出失败", "module", "relay", "operation", "controlHTTP", "error", err) + } case req.Method == "POST" && req.URL.Path == "/_control/access-lease": raw, err := io.ReadAll(http.MaxBytesReader(w, req.Body, maxControlMessage)) var in LeaseRequest if err != nil || decodeMessage(raw, &in) != nil { w.WriteHeader(400) - json.NewEncoder(w).Encode(leaseResponse{Code: "invalid_tunnel_message"}) + if err := json.NewEncoder(w).Encode(leaseResponse{Code: "invalid_tunnel_message"}); err != nil { + slog.Error("写入输出失败", "module", "relay", "operation", "controlHTTP", "error", err) + } return } result, status := s.lease(req.Context(), r, in) - _ = rc.SetWriteDeadline(time.Now().Add(5 * time.Second)) + if err := rc.SetWriteDeadline(time.Now().Add(5 * time.Second)); err != nil { + slog.Error("设置连接截止时间失败", "module", "relay", "operation", "controlHTTP", "error", err) + } w.WriteHeader(status) - json.NewEncoder(w).Encode(result) + if err := json.NewEncoder(w).Encode(result); err != nil { + slog.Error("写入输出失败", "module", "relay", "operation", "controlHTTP", "error", err) + } case req.Method == "GET" && req.URL.Path == "/_control/events": s.mu.Lock() duplicate := r.eventsActive @@ -384,15 +415,23 @@ func (s *Service) controlHTTP(w http.ResponseWriter, req *http.Request, r *route return } defer func() { s.closeRoute(r) }() - _ = rc.SetReadDeadline(time.Time{}) + if err := rc.SetReadDeadline(time.Time{}); err != nil { + slog.Error("设置连接截止时间失败", "module", "relay", "operation", "controlHTTP", "error", err) + } w.Header().Set("Content-Type", "application/x-ndjson") send := func(value any) bool { - _ = rc.SetWriteDeadline(time.Now().Add(5 * time.Second)) + if err := rc.SetWriteDeadline(time.Now().Add(5 * time.Second)); err != nil { + slog.Error("设置连接截止时间失败", "module", "relay", "operation", "controlHTTP", "error", err) + } raw, err := json.Marshal(value) if err != nil || len(raw) > maxControlMessage { + if err != nil { + slog.WarnContext(req.Context(), "操作失败", "module", "relay", "operation", "controlHTTP", "error", err) + } return false } if _, err = w.Write(append(raw, '\n')); err != nil { + slog.WarnContext(req.Context(), "操作失败", "module", "relay", "operation", "controlHTTP", "error", err) return false } return rc.Flush() == nil @@ -418,7 +457,9 @@ func (s *Service) controlHTTP(w http.ResponseWriter, req *http.Request, r *route } default: w.WriteHeader(404) - json.NewEncoder(w).Encode(map[string]string{"code": "not_found"}) + if err := json.NewEncoder(w).Encode(map[string]string{"code": "not_found"}); err != nil { + slog.Error("写入输出失败", "module", "relay", "operation", "controlHTTP", "error", err) + } } } @@ -488,6 +529,9 @@ func (s *Service) monitor(r *route, stop <-chan struct{}) { continue } if a.id == "" || verifyErr != nil { + if verifyErr != nil { + slog.Warn("操作失败", "module", "relay", "operation", "monitor", "error", verifyErr) + } continue } row, found := states[a.id] diff --git a/monkeyai/backend/internal/relay/preview.go b/monkeyai/backend/internal/relay/preview.go index 4d6e7863f..a0758f62e 100644 --- a/monkeyai/backend/internal/relay/preview.go +++ b/monkeyai/backend/internal/relay/preview.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "errors" + "log/slog" "net" "net/http" "net/url" @@ -23,6 +24,9 @@ func validatePreviewTemplate(template, control, files string) error { } u, err := url.Parse(strings.Replace(template, "{artifact}", strings.Repeat("a", 32), 1)) if err != nil || u.User != nil || u.Path != "" || u.RawQuery != "" || u.Fragment != "" || u.Port() != "" { + if err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "validatePreviewTemplate", "error", err) + } return errors.New("预览 origin 模板不能包含端口、路径或凭据") } base := strings.TrimPrefix(u.Hostname(), strings.Repeat("a", 32)+".") @@ -51,14 +55,18 @@ func previewHost(template, host string) bool { if template == "" { return false } - u, _ := url.Parse(strings.Replace(template, "{artifact}", "artifact", 1)) + u, err := url.Parse(strings.Replace(template, "{artifact}", "artifact", 1)) + if err != nil { + slog.Error("解析预览域名失败", "error", err) + return false + } base := strings.TrimPrefix(u.Host, "artifact.") host = strings.ToLower(host) if !strings.HasSuffix(host, "."+base) { return false } label := strings.TrimSuffix(host, "."+base) - _, err := hex.DecodeString(label) + _, err = hex.DecodeString(label) return len(label) == 32 && err == nil } @@ -71,6 +79,7 @@ func previewDomain(template, host string) bool { } u, err := url.Parse(strings.Replace(template, "{artifact}", "artifact", 1)) if err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "previewDomain", "error", err) return false } base := strings.TrimPrefix(u.Host, "artifact.") @@ -104,6 +113,7 @@ func (s *Service) preview(w http.ResponseWriter, r *http.Request) { if r.URL.RawPath != "" { decoded, err := url.PathUnescape(requested) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "preview", "error", err) failure(r.Context(), w, 400, "invalid_request") return } @@ -119,6 +129,9 @@ func (s *Service) preview(w http.ResponseWriter, r *http.Request) { expected := PreviewOrigin(s.config.PreviewOriginTemplate, grant.ServerID, grant.OwnerUserID, grant.MachineID, grant.SessionID, grant.ArtifactID) u, err := url.Parse(expected) if err != nil || !validUUID(grant.ArtifactID) || grant.PreviewOrigin != expected || r.Host != u.Host || grant.Scope != "directory" { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "preview", "error", err) + } failure(r.Context(), w, 403, "permission_denied") return } diff --git a/monkeyai/backend/internal/relay/protocol.go b/monkeyai/backend/internal/relay/protocol.go index b861de5cc..622abb3e7 100644 --- a/monkeyai/backend/internal/relay/protocol.go +++ b/monkeyai/backend/internal/relay/protocol.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "io" + "log/slog" "reflect" "slices" "strings" @@ -126,6 +127,9 @@ func validateHello(h Hello, channel string, tunnelVersion int) ([]byte, error) { } public, err := base64.RawURLEncoding.Strict().DecodeString(h.ExecutionPublicKey) if err != nil || len(public) != ed25519.PublicKeySize { + if err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "validateHello", "error", err) + } return nil, errProtocol } return public, nil @@ -138,6 +142,7 @@ func decodeMessage(data []byte, out any) error { check := json.NewDecoder(bytes.NewReader(data)) check.UseNumber() if err := checkJSON(check, 0); err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "decodeMessage", "error", err) return errProtocol } if _, err := check.Token(); err != io.EOF { @@ -149,6 +154,7 @@ func decodeMessage(data []byte, out any) error { decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields() if err := decoder.Decode(out); err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "decodeMessage", "error", err) return errProtocol } return nil @@ -233,6 +239,9 @@ func checkJSON(d *json.Decoder, depth int) error { } end, err := d.Token() if err != nil || delim == '{' && end != json.Delim('}') || delim == '[' && end != json.Delim(']') { + if err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "checkJSON", "error", err) + } return errProtocol } return nil diff --git a/monkeyai/backend/internal/relay/proxy.go b/monkeyai/backend/internal/relay/proxy.go index 5bfa0060f..07e9ded55 100644 --- a/monkeyai/backend/internal/relay/proxy.go +++ b/monkeyai/backend/internal/relay/proxy.go @@ -102,6 +102,9 @@ func (s *Service) proxy(w http.ResponseWriter, r *http.Request) { machine := chi.URLParam(r, "machine") p, pathErr := requestPath(r, "/"+chi.URLParam(r, "*")) if pathErr != nil || !validUUID(machine) || !safePath(p) { + if pathErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "proxy", "error", pathErr) + } failure(r.Context(), w, 400, "invalid_request") return } @@ -253,6 +256,7 @@ func (s *Service) proxy(w http.ResponseWriter, r *http.Request) { "authorization": map[string]any{"server_id": s.config.ServerID, "owner_user_id": row.UserID, "machine_id": machine, "connection_id": row.ID, "client_grant_id": row.ClientGrantID, "grant_version": row.GrantVersion, "access_version": row.AccessVersion, "lease_epoch": current.state.LeaseEpoch, "scopes": row.Scopes}, "expires_at": row.ExpiresAt.UTC().Format(time.RFC3339Nano)}) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "proxy", "error", err) failure(r.Context(), w, 503, "authorization_state_unavailable") return } @@ -394,6 +398,7 @@ func (s *Service) forward(w http.ResponseWriter, r *http.Request, route *route, DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { stream, err := s.openStream(ctx, mux, opens, a, permit) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "forward", "error", err) s.metrics.openFailed.Add(1) } return stream, err @@ -431,7 +436,9 @@ func (s *Service) forward(w http.ResponseWriter, r *http.Request, route *route, } if terminal { if response.StatusCode == http.StatusSwitchingProtocols && !validTerminalUpgrade(response, r.Header.Get("Sec-WebSocket-Key")) { - response.Body.Close() + if err := response.Body.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "forward", "error", err) + } return errors.New("执行端终端 Upgrade 无效") } handshakeTimer.Stop() diff --git a/monkeyai/backend/internal/relay/service.go b/monkeyai/backend/internal/relay/service.go index 2b60ff045..1358e28ce 100644 --- a/monkeyai/backend/internal/relay/service.go +++ b/monkeyai/backend/internal/relay/service.go @@ -7,7 +7,6 @@ import ( "crypto/sha256" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "hash/fnv" "io" "log/slog" @@ -19,6 +18,8 @@ import ( "sync/atomic" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/endpoint" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/relay/sqlc" @@ -109,6 +110,9 @@ func NewService(pool *pgxpool.Pool, identities *identity.Service, devices *endpo } { u, err := url.Parse(origin.value) if err != nil || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" || u.Path != "" || (u.Scheme != "http" && u.Scheme != "https") { + if err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "NewService", "error", err) + } return nil, fmt.Errorf("%s 必须为不含账号、路径、查询参数或片段的 HTTP(S) origin", origin.name) } } @@ -118,6 +122,9 @@ func NewService(pool *pgxpool.Pool, identities *identity.Service, devices *endpo } u, err := url.Parse(origin) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.User != nil || u.Path != "" || u.RawQuery != "" || u.Fragment != "" || u.ForceQuery { + if err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "NewService", "error", err) + } return nil, errors.New("Desktop Origin 必须为完整 HTTP(S) origin") } } @@ -282,13 +289,19 @@ func (s *Service) closeRoute(r *route) { a.close() } if data != nil { - data.Close() + if err := data.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "closeRoute", "error", err) + } } if control != nil { - control.Close() + if err := control.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "closeRoute", "error", err) + } } if r.ws != nil { - r.ws.CloseNow() + if err := r.ws.CloseNow(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "closeRoute", "error", err) + } } } @@ -346,7 +359,9 @@ func (s *Service) Shutdown(ctx context.Context) error { s.closeRoute(r) } for _, ws := range connections { - ws.CloseNow() + if err := ws.CloseNow(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "Shutdown", "error", err) + } } finished := make(chan struct{}) go func() { s.workers.Wait(); close(finished) }() @@ -382,6 +397,9 @@ func (s *Service) revoke(w http.ResponseWriter, r *http.Request) { versionText := strings.Trim(r.Header.Get("If-Match"), "\"") version, err := strconv.ParseInt(versionText, 10, 64) if err != nil || version < 0 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "revoke", "error", err) + } failure(r.Context(), w, http.StatusPreconditionRequired, "device_registration_changed") return } diff --git a/monkeyai/backend/internal/relay/signing.go b/monkeyai/backend/internal/relay/signing.go index 1278bc385..15e12a974 100644 --- a/monkeyai/backend/internal/relay/signing.go +++ b/monkeyai/backend/internal/relay/signing.go @@ -9,6 +9,7 @@ import ( "encoding/json" "encoding/pem" "errors" + "log/slog" "os" "strings" @@ -65,6 +66,9 @@ func normalizeVerificationKey(key map[string]string) (map[string]string, error) } public, err := base64.RawURLEncoding.Strict().DecodeString(encoded) if err != nil || len(public) != ed25519.PublicKeySize || key["kid"] != endpoint.KeyID(public) { + if err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "normalizeVerificationKey", "error", err) + } return nil, errors.New("中转公钥集合包含无效 Ed25519 公钥") } return verificationKey(ed25519.PublicKey(public)), nil @@ -127,10 +131,16 @@ func verifyWire(public ed25519.PublicKey, wire string) (json.RawMessage, bool) { } payload, err := base64.RawURLEncoding.Strict().DecodeString(parts[0]) if err != nil || len(payload) == 0 || len(payload) > 8<<10 { + if err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "verifyWire", "error", err) + } return nil, false } signature, err := base64.RawURLEncoding.Strict().DecodeString(parts[1]) if err != nil || !ed25519.Verify(public, []byte("monkeyai.access.signed.v1."+parts[0]), signature) { + if err != nil { + slog.Warn("操作失败", "module", "relay", "operation", "verifyWire", "error", err) + } return nil, false } return payload, true diff --git a/monkeyai/backend/internal/relay/stream.go b/monkeyai/backend/internal/relay/stream.go index 6bb192e14..2ff7ff147 100644 --- a/monkeyai/backend/internal/relay/stream.go +++ b/monkeyai/backend/internal/relay/stream.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "log/slog" "net" "time" @@ -17,7 +18,9 @@ type meteredConn struct { func (c *meteredConn) Close() error { // yamux.Close 是半关闭;先中断读写,确保代理两侧都能退出。 - c.Conn.SetDeadline(time.Now()) + if err := c.Conn.SetDeadline(time.Now()); err != nil { + slog.Error("设置连接截止时间失败", "module", "relay", "operation", "Close", "error", err) + } return c.Conn.Close() } @@ -78,18 +81,24 @@ func (s *Service) openStream(ctx context.Context, mux *yamux.Session, opens chan a.mu.Lock() if a.closed || ctx.Err() != nil { a.mu.Unlock() - conn.Close() + if err := conn.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "openStream", "error", err) + } return } a.stream = conn - conn.SetDeadline(minTime(a.expiresAt, a.verifiedAt.Add(180*time.Second))) + if err := conn.SetDeadline(minTime(a.expiresAt, a.verifiedAt.Add(180*time.Second))); err != nil { + slog.Error("设置连接截止时间失败", "module", "relay", "operation", "openStream", "error", err) + } a.mu.Unlock() } select { case done <- result{conn, err}: case <-ctx.Done(): if conn != nil { - conn.Close() + if err := conn.Close(); err != nil { + slog.Error("关闭资源失败", "module", "relay", "operation", "openStream", "error", err) + } } } }() diff --git a/monkeyai/backend/internal/relay/terminal.go b/monkeyai/backend/internal/relay/terminal.go index 97c136659..61c10c3f9 100644 --- a/monkeyai/backend/internal/relay/terminal.go +++ b/monkeyai/backend/internal/relay/terminal.go @@ -3,6 +3,7 @@ package relay import ( "crypto/sha1" "encoding/base64" + "log/slog" "net/http" "net/url" "slices" @@ -32,6 +33,9 @@ func terminalHandshake(r *http.Request) (string, bool, bool) { } key, err := base64.StdEncoding.Strict().DecodeString(keys[0]) if err != nil || len(key) != 16 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "terminalHandshake", "error", err) + } return "", false, false } if len(r.URL.RawQuery) > 64 { @@ -40,6 +44,9 @@ func terminalHandshake(r *http.Request) (string, bool, bool) { query, err := url.ParseQuery(r.URL.RawQuery) candidate := false if err != nil || r.URL.ForceQuery { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "relay", "operation", "terminalHandshake", "error", err) + } return "", false, false } if r.URL.RawQuery != "" { diff --git a/monkeyai/backend/internal/resource/grants.go b/monkeyai/backend/internal/resource/grants.go index f4c5fa057..bfd05cf64 100644 --- a/monkeyai/backend/internal/resource/grants.go +++ b/monkeyai/backend/internal/resource/grants.go @@ -5,15 +5,14 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" - "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource/sqlc" - "github.com/jackc/pgx/v5" - "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource/sqlc" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/rootgroup" - "github.com/go-chi/chi/v5" + "github.com/jackc/pgx/v5" ) func checkRetiredGrants(ctx context.Context, tx pgx.Tx, kind, id string, raw any) error { @@ -42,6 +41,9 @@ func checkRetiredGrants(ctx context.Context, tx pgx.Tx, kind, id string, raw any var requested []Object data, err := json.Marshal(raw) if err != nil || json.Unmarshal(data, &requested) != nil { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "resource", "operation", "checkRetiredGrants", "error", err) + } return Invalid("授权格式无效") } for _, entry := range requested { diff --git a/monkeyai/backend/internal/resource/page.go b/monkeyai/backend/internal/resource/page.go index 57e0afbd3..d06bd1fb5 100644 --- a/monkeyai/backend/internal/resource/page.go +++ b/monkeyai/backend/internal/resource/page.go @@ -1,6 +1,7 @@ package resource import ( + "log/slog" "net/http" "strconv" ) @@ -19,6 +20,9 @@ func PageParamsWithLimit(r *http.Request, maxSize int) (page, size int, err erro if raw := r.URL.Query().Get(field.name); raw != "" { n, parseErr := strconv.Atoi(raw) if parseErr != nil || n < 1 || n > field.max { + if parseErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resource", "operation", "PageParamsWithLimit", "error", parseErr) + } return 0, 0, Invalid(field.name + " 无效") } *field.value = n diff --git a/monkeyai/backend/internal/resource/store.go b/monkeyai/backend/internal/resource/store.go index 992433eb3..6e4a3c37f 100644 --- a/monkeyai/backend/internal/resource/store.go +++ b/monkeyai/backend/internal/resource/store.go @@ -117,6 +117,7 @@ func Decode(w http.ResponseWriter, r *http.Request, v any) error { d := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) var body json.RawMessage if err := d.Decode(&body); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resource", "operation", "Decode", "error", err) return Invalid("请求格式无效") } if data := bytes.TrimSpace(body); len(data) == 0 || data[0] != '{' { @@ -126,6 +127,7 @@ func Decode(w http.ResponseWriter, r *http.Request, v any) error { return Invalid("请求只能包含一个 JSON 对象") } if err := json.Unmarshal(body, v); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resource", "operation", "Decode", "error", err) return Invalid("请求格式无效") } return nil @@ -177,6 +179,7 @@ func SaveGrants(ctx context.Context, tx pgx.Tx, kind, id, actor string, raw any, Usage string `json:"usage_requirement"` } if err := json.Unmarshal(b, &grants); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "resource", "operation", "SaveGrants", "error", err) return Invalid("授权格式无效") } if _, err := sqlc.New(tx).DeleteGrants(ctx, sqlc.DeleteGrantsParams{ResourceType: kind, ResourceID: id}); err != nil { @@ -629,6 +632,9 @@ func (c *CRUD) Register(r chi.Router) { if raw := r.URL.Query().Get("limit"); raw != "" { n, err := strconv.Atoi(raw) if err != nil || n < 1 || n > 500 { + if err != nil { + slog.Warn("操作失败", "module", "resource", "operation", "Register", "error", err) + } Fail(r.Context(), w, Invalid("limit 必须在 1—500 之间")) return } diff --git a/monkeyai/backend/internal/resource/tags.go b/monkeyai/backend/internal/resource/tags.go index 7b6f5eadf..76a4a16c3 100644 --- a/monkeyai/backend/internal/resource/tags.go +++ b/monkeyai/backend/internal/resource/tags.go @@ -3,6 +3,7 @@ package resource import ( "context" "encoding/json" + "log/slog" "net/http" "slices" "strings" @@ -80,6 +81,7 @@ func Tags(ctx context.Context, q Queryer, kind, id string) ([]Object, error) { func SaveTags(ctx context.Context, q Queryer, kind, id, actor string, raw any) error { data, err := json.Marshal(raw) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "resource", "operation", "SaveTags", "error", err) return Invalid("标签格式无效") } var ids []string diff --git a/monkeyai/backend/internal/resourceimport/artifact.go b/monkeyai/backend/internal/resourceimport/artifact.go index 19944ede2..680840c68 100644 --- a/monkeyai/backend/internal/resourceimport/artifact.go +++ b/monkeyai/backend/internal/resourceimport/artifact.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "log/slog" "maps" "slices" "strings" @@ -218,7 +219,11 @@ func normalizeDescription(data []byte) ([]byte, string, bool) { if !strings.Contains(value, ": ") || value == "" || strings.ContainsAny(value[:1], `"'|>`) { return nil, "", false } - quoted, _ := json.Marshal(value) + quoted, err := json.Marshal(value) + if err != nil { + slog.Error("编码资源属性失败", "error", err) + return nil, "", false + } ending := "\n" if strings.HasSuffix(original, "\r\n") { ending = "\r\n" diff --git a/monkeyai/backend/internal/resourceimport/design.go b/monkeyai/backend/internal/resourceimport/design.go index 1f7b4bd02..968d25578 100644 --- a/monkeyai/backend/internal/resourceimport/design.go +++ b/monkeyai/backend/internal/resourceimport/design.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "log/slog" "net/http" "slices" "time" @@ -266,7 +267,9 @@ func (s *Service) applySections(ctx context.Context, loaded loadedImport) (Secti if err == nil { err = tx.Commit(ctx) } - _ = tx.Rollback(context.WithoutCancel(ctx)) + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "resourceimport", "operation", "applySections", "error", err) + } } } } @@ -285,6 +288,7 @@ func (s *Service) applySections(ctx context.Context, loaded loadedImport) (Secti recordErr := saveSection(recordCtx, s.Pool, loaded, section, plan.PlanDigest, result) cancel() if recordErr != nil { + slog.WarnContext(ctx, "操作失败", "module", "resourceimport", "operation", "applySections", "error", recordErr) result.Error = &SectionError{"service_unavailable", "分区结果未能持久化,请重新预览恢复状态"} result.HTTPStatus = 503 failureStatus = 503 diff --git a/monkeyai/backend/internal/resourceimport/load.go b/monkeyai/backend/internal/resourceimport/load.go index e8ae57711..6a4f79bdd 100644 --- a/monkeyai/backend/internal/resourceimport/load.go +++ b/monkeyai/backend/internal/resourceimport/load.go @@ -5,9 +5,12 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "io" + "log/slog" "mime/multipart" "net/http" + "os" "strings" "sync" @@ -57,6 +60,7 @@ func (s *Service) loadImport(w http.ResponseWriter, r *http.Request) (loaded loa }) } if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resourceimport", "operation", "loadImport", "error", err) loaded.cleanup() } }() @@ -71,6 +75,7 @@ func (s *Service) loadImport(w http.ResponseWriter, r *http.Request) (loaded loa } if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") { if err = resource.Decode(w, r, &loaded.input); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resourceimport", "operation", "loadImport", "error", err) return } actor, ok := identity.UserFromContext(r.Context()) @@ -81,9 +86,14 @@ func (s *Service) loadImport(w http.ResponseWriter, r *http.Request) (loaded loa var upload Upload upload, loaded.tx, err = s.lockUpload(r.Context(), loaded.input.UploadID, actor.ID) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resourceimport", "operation", "loadImport", "error", err) return } - loaded.cleanup = func() { _ = loaded.tx.Rollback(context.WithoutCancel(r.Context())) } + loaded.cleanup = func() { + if err := loaded.tx.Rollback(context.WithoutCancel(r.Context())); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "resourceimport", "operation", "loadImport", "error", err) + } + } if upload.Status != "ready" && upload.Status != "imported" { err = resource.Invalid("请先完成资源包上传和完整性检查") return @@ -91,11 +101,14 @@ func (s *Service) loadImport(w http.ResponseWriter, r *http.Request) (loaded loa var cleanup func() reader, loaded.sha, cleanup, err = s.openUpload(r.Context(), upload) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resourceimport", "operation", "loadImport", "error", err) return } loaded.cleanup = func() { cleanup() - _ = loaded.tx.Rollback(context.WithoutCancel(r.Context())) + if err := loaded.tx.Rollback(context.WithoutCancel(r.Context())); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "resourceimport", "operation", "loadImport", "error", err) + } } size = upload.TotalSize } else { @@ -103,10 +116,14 @@ func (s *Service) loadImport(w http.ResponseWriter, r *http.Request) (loaded loa var file multipart.File loaded.cleanup = func() { if file != nil { - _ = file.Close() + if err := file.Close(); err != nil { + slog.Error("关闭资源失败", "module", "resourceimport", "operation", "loadImport", "error", err) + } } if r.MultipartForm != nil { - _ = r.MultipartForm.RemoveAll() + if err := r.MultipartForm.RemoveAll(); err != nil && !os.IsNotExist(err) { + slog.Error("清理临时文件失败", "module", "resourceimport", "operation", "loadImport", "error", err) + } } } if err = r.ParseMultipartForm(8 << 20); err != nil { @@ -145,6 +162,7 @@ func (s *Service) loadImport(w http.ResponseWriter, r *http.Request) (loaded loa loaded.input.DiscoverOnly = r.FormValue("discover_only") == "true" hash := sha256.New() if _, err = io.Copy(hash, io.NewSectionReader(file, 0, header.Size)); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resourceimport", "operation", "loadImport", "error", err) return } loaded.sha = "sha256:" + hex.EncodeToString(hash.Sum(nil)) @@ -160,6 +178,7 @@ func (s *Service) loadImport(w http.ResponseWriter, r *http.Request) (loaded loa } loaded.sections, err = selectSections(loaded.input.Sections, loaded.inspection) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resourceimport", "operation", "loadImport", "error", err) return } if containsSection(loaded.sections, "design") { @@ -174,7 +193,12 @@ func (s *Service) loadImport(w http.ResponseWriter, r *http.Request) (loaded loa } previous := loaded.cleanup pkg := loaded.design - loaded.cleanup = func() { pkg.Close(); previous() } + loaded.cleanup = func() { + if err := pkg.Close(); err != nil { + slog.Error("关闭资源失败", "module", "resourceimport", "operation", "loadImport", "error", err) + } + previous() + } } if containsSection(loaded.sections, "agent") { loaded.pkg, err = ParseReader(reader, size) diff --git a/monkeyai/backend/internal/resourceimport/package.go b/monkeyai/backend/internal/resourceimport/package.go index c930a9874..d7ce01fa8 100644 --- a/monkeyai/backend/internal/resourceimport/package.go +++ b/monkeyai/backend/internal/resourceimport/package.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "os" "path" "regexp" @@ -120,6 +121,9 @@ func (p Package) ReadFile(name string) ([]byte, error) { data, readErr := io.ReadAll(io.LimitReader(reader, int64(entry.UncompressedSize64)+1)) closeErr := reader.Close() if readErr != nil || closeErr != nil || uint64(len(data)) != entry.UncompressedSize64 { + if closeErr != nil { + slog.Warn("操作失败", "module", "resourceimport", "operation", "ReadFile", "error", closeErr) + } return nil, fmt.Errorf("资源包中文件损坏: %s", name) } return data, nil @@ -189,12 +193,17 @@ func readZIP(data []byte, maxSize, maxExpanded, maxFile int) (map[string][]byte, } limit := min(maxFile, maxExpanded-total) if limit < 0 { - reader.Close() + if err := reader.Close(); err != nil { + slog.Error("关闭资源失败", "module", "resourceimport", "operation", "readZIP", "error", err) + } return nil, fmt.Errorf("ZIP 解压大小超限") } content, readErr := io.ReadAll(io.LimitReader(reader, int64(limit)+1)) closeErr := reader.Close() if readErr != nil || closeErr != nil || len(content) > limit || uint64(len(content)) != f.UncompressedSize64 { + if closeErr != nil { + slog.Warn("操作失败", "module", "resourceimport", "operation", "readZIP", "error", closeErr) + } return nil, fmt.Errorf("ZIP 文件损坏或解压大小超限: %q", name) } total += len(content) @@ -229,7 +238,9 @@ func decodeJSON(data []byte, out any) error { return fmt.Errorf("JSON 格式无效") } // 字段类型不匹配时保留可读取的字段,其余使用导入默认值。 - _ = json.Unmarshal(data, out) + if err := json.Unmarshal(data, out); err != nil { + slog.Error("解析数据失败", "module", "resourceimport", "operation", "decodeJSON", "error", err) + } return nil } diff --git a/monkeyai/backend/internal/resourceimport/reader.go b/monkeyai/backend/internal/resourceimport/reader.go index 3960e9c9f..2a08b9999 100644 --- a/monkeyai/backend/internal/resourceimport/reader.go +++ b/monkeyai/backend/internal/resourceimport/reader.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "io" + "log/slog" "os" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" @@ -50,8 +51,12 @@ func (s *Service) openUpload(ctx context.Context, upload Upload) (*archiveReader reader := &archiveReader{} cleanup := func() { for _, file := range reader.files { - _ = file.Close() - _ = os.Remove(file.Name()) + if err := file.Close(); err != nil { + slog.Error("关闭资源失败", "module", "resourceimport", "operation", "openUpload", "error", err) + } + if err := os.Remove(file.Name()); err != nil && !os.IsNotExist(err) { + slog.Error("清理临时文件失败", "module", "resourceimport", "operation", "openUpload", "error", err) + } } } fail := func(err error) (*archiveReader, string, func(), error) { @@ -72,7 +77,9 @@ func (s *Service) openUpload(ctx context.Context, upload Upload) (*archiveReader } file, err := os.CreateTemp("", "resource-import-*") if err != nil { - _ = stream.Close() + if err := stream.Close(); err != nil { + slog.Error("关闭资源失败", "module", "resourceimport", "operation", "openUpload", "error", err) + } return fail(err) } reader.files = append(reader.files, file) @@ -80,7 +87,13 @@ func (s *Service) openUpload(ctx context.Context, upload Upload) (*archiveReader chunkHash := sha256.New() n, readErr := io.Copy(io.MultiWriter(file, hash, chunkHash), io.LimitReader(stream, chunk.Size+1)) closeErr := stream.Close() - if readErr != nil || closeErr != nil || n != chunk.Size || "sha256:"+hex.EncodeToString(chunkHash.Sum(nil)) != chunk.SHA256 { + if readErr != nil { + return fail(readErr) + } + if closeErr != nil { + return fail(closeErr) + } + if n != chunk.Size || "sha256:"+hex.EncodeToString(chunkHash.Sum(nil)) != chunk.SHA256 { return fail(resource.Invalid(fmt.Sprintf("资源包传输块 %d 损坏,请重新上传", index+1))) } } diff --git a/monkeyai/backend/internal/resourceimport/service.go b/monkeyai/backend/internal/resourceimport/service.go index 7c289c4b1..2a66294a4 100644 --- a/monkeyai/backend/internal/resourceimport/service.go +++ b/monkeyai/backend/internal/resourceimport/service.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "hash/fnv" "log/slog" "net/http" @@ -17,6 +16,8 @@ import ( "time" "uuid" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resourceimport/sqlc" @@ -39,6 +40,9 @@ func decodeHistoryCursor(value string) ([]byte, error) { } data, err := base64.RawURLEncoding.DecodeString(value) if err != nil || len(data) > 256 { + if err != nil { + slog.Warn("操作失败", "module", "resourceimport", "operation", "decodeHistoryCursor", "error", err) + } return nil, resource.Invalid("分页游标无效") } var cursor historyCursor @@ -46,6 +50,7 @@ func decodeHistoryCursor(value string) ([]byte, error) { return nil, resource.Invalid("分页游标无效") } if _, err = uuid.Parse(cursor.ID); err != nil { + slog.Warn("操作失败", "module", "resourceimport", "operation", "decodeHistoryCursor", "error", err) return nil, resource.Invalid("分页游标无效") } return json.Marshal(cursor) @@ -102,6 +107,7 @@ func (s *Service) RegisterAdmin(router chi.Router) { return } if err = s.finishSections(r.Context(), loaded); err != nil { + slog.Warn("操作失败", "module", "resourceimport", "operation", "RegisterAdmin", "error", err) out.Warning = "分区已提交,上传状态未更新;请刷新恢复" } resource.JSON(r.Context(), w, status, out) @@ -133,6 +139,9 @@ func (s *Service) RegisterAdmin(router chi.Router) { if raw := r.URL.Query().Get("limit"); raw != "" { n, err := strconv.Atoi(raw) if err != nil || n < 1 || n > 500 { + if err != nil { + slog.Warn("操作失败", "module", "resourceimport", "operation", "RegisterAdmin", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("limit 必须为 1 至 500")) return } @@ -181,6 +190,7 @@ func (s *Service) RegisterAdmin(router chi.Router) { router.Get("/resource-imports/{id}", func(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if _, err := uuid.Parse(id); err != nil { + slog.Warn("操作失败", "module", "resourceimport", "operation", "RegisterAdmin", "error", err) resource.Fail(r.Context(), w, resource.NotFound) return } @@ -368,7 +378,11 @@ func (s *Service) apply(ctx context.Context, p Package, r Resources, opts Option if err != nil { return pre, err } - defer tx.Rollback(context.WithoutCancel(ctx)) + defer func() { + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "resourceimport", "operation", "apply", "error", err) + } + }() if err = s.lockBindings(ctx, tx, pre.Publisher); err != nil { return pre, err } @@ -671,7 +685,11 @@ func mapOrEmpty(value map[string]string) map[string]string { } func payload(value any) []byte { - data, _ := json.Marshal(value) + data, err := json.Marshal(value) + if err != nil { + slog.Error("编码资源数据失败", "error", err) + return nil + } return data } diff --git a/monkeyai/backend/internal/resourceimport/upload.go b/monkeyai/backend/internal/resourceimport/upload.go index 076d6b9fd..741867348 100644 --- a/monkeyai/backend/internal/resourceimport/upload.go +++ b/monkeyai/backend/internal/resourceimport/upload.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net/http" @@ -18,6 +17,8 @@ import ( "unicode/utf8" "uuid" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resourceimport/sqlc" @@ -70,7 +71,10 @@ func validateUpload(files []UploadFile) (int64, error) { if match == nil { return 0, resource.Invalid("请选择普通 ZIP,或按字节拆分的 .zip.001、.002 等全部分卷;不支持 .z01 或 7z 分卷") } - n, _ := strconv.Atoi(match[2]) + n, err := strconv.Atoi(match[2]) + if err != nil { + return 0, resource.Invalid(err.Error()) + } if n != i+1 || (base != "" && !strings.EqualFold(base, match[1])) { return 0, resource.Invalid("分卷编号必须从 001 连续递增,且属于同一资源包") } @@ -114,6 +118,7 @@ func (s *Service) uploadChunks(ctx context.Context, q resource.Queryer, upload * func (s *Service) lockUpload(ctx context.Context, id, actor string) (Upload, pgx.Tx, error) { if _, err := uuid.Parse(id); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "resourceimport", "operation", "lockUpload", "error", err) return Upload{}, nil, resource.NotFound } logging.Bind(ctx, slog.String("upload_id", id), slog.String("module", "resourceimport"), slog.String("phase", "lock_upload")) @@ -123,7 +128,9 @@ func (s *Service) lockUpload(ctx context.Context, id, actor string) (Upload, pgx } data, err := sqlc.New(tx).LockImportUpload(ctx, sqlc.LockImportUploadParams{ID: id, ActorUserID: actor}) if err != nil { - _ = tx.Rollback(context.WithoutCancel(ctx)) + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "resourceimport", "operation", "lockUpload", "error", err) + } return Upload{}, nil, err } upload, err := decodeUpload(data) @@ -131,7 +138,9 @@ func (s *Service) lockUpload(ctx context.Context, id, actor string) (Upload, pgx err = s.uploadChunks(ctx, tx, &upload) } if err != nil { - _ = tx.Rollback(context.WithoutCancel(ctx)) + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "resourceimport", "operation", "lockUpload", "error", err) + } return Upload{}, nil, err } return upload, tx, nil @@ -216,6 +225,7 @@ func (s *Service) listUploads(w http.ResponseWriter, r *http.Request) { func (s *Service) getUpload(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if _, err := uuid.Parse(id); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resourceimport", "operation", "getUpload", "error", err) resource.Fail(r.Context(), w, resource.NotFound) return } @@ -243,10 +253,17 @@ func (s *Service) putChunk(w http.ResponseWriter, r *http.Request) { resource.Fail(r.Context(), w, err) return } - defer tx.Rollback(context.WithoutCancel(r.Context())) + defer func() { + if err := tx.Rollback(context.WithoutCancel(r.Context())); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "resourceimport", "operation", "putChunk", "error", err) + } + }() index, err := strconv.Atoi(chi.URLParam(r, "index")) size := upload.chunkSize(index) if err != nil || size == 0 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resourceimport", "operation", "putChunk", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("传输块编号无效")) return } @@ -265,10 +282,20 @@ func (s *Service) putChunk(w http.ResponseWriter, r *http.Request) { resource.Fail(r.Context(), w, err) return } - defer func() { _ = file.Close(); _ = os.Remove(file.Name()) }() + defer func() { + if err := file.Close(); err != nil { + slog.Error("关闭资源失败", "module", "resourceimport", "operation", "putChunk", "error", err) + } + if err := os.Remove(file.Name()); err != nil && !os.IsNotExist(err) { + slog.Error("清理临时文件失败", "module", "resourceimport", "operation", "putChunk", "error", err) + } + }() hash := sha256.New() length, err := io.Copy(io.MultiWriter(file, hash), http.MaxBytesReader(w, r.Body, size)) if err != nil || length != size || "sha256:"+hex.EncodeToString(hash.Sum(nil)) != expected { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resourceimport", "operation", "putChunk", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("传输块大小或校验值不匹配,请重试")) return } @@ -323,7 +350,11 @@ func (s *Service) completeUpload(w http.ResponseWriter, r *http.Request) { resource.Fail(r.Context(), w, err) return } - defer tx.Rollback(context.WithoutCancel(r.Context())) + defer func() { + if err := tx.Rollback(context.WithoutCancel(r.Context())); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "resourceimport", "operation", "completeUpload", "error", err) + } + }() if upload.Status != "uploading" { resource.JSON(r.Context(), w, http.StatusOK, upload) return @@ -346,6 +377,9 @@ func (s *Service) completeUpload(w http.ResponseWriter, r *http.Request) { n, readErr := io.Copy(io.Discard, io.LimitReader(stream, int64(entry.UncompressedSize64)+1)) closeErr := stream.Close() if readErr != nil || closeErr != nil || uint64(n) != entry.UncompressedSize64 { + if closeErr != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "resourceimport", "operation", "completeUpload", "error", closeErr) + } err = fmt.Errorf("文件 %s 损坏,请检查分卷是否完整且来自同一资源包", name) break } @@ -375,7 +409,11 @@ func (s *Service) cancelUpload(w http.ResponseWriter, r *http.Request) { resource.Fail(r.Context(), w, err) return } - defer tx.Rollback(context.WithoutCancel(r.Context())) + defer func() { + if err := tx.Rollback(context.WithoutCancel(r.Context())); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "resourceimport", "operation", "cancelUpload", "error", err) + } + }() if upload.Status == "imported" { resource.Fail(r.Context(), w, conflict("该任务已完成导入")) return @@ -396,7 +434,11 @@ func (s *Service) CleanupUploads(ctx context.Context) error { if err != nil { return err } - defer tx.Rollback(context.WithoutCancel(ctx)) + defer func() { + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "resourceimport", "operation", "CleanupUploads", "error", err) + } + }() rows, err := sqlc.New(tx).ListExpiredImportUploads(ctx) if err != nil { return err diff --git a/monkeyai/backend/internal/session/agent.go b/monkeyai/backend/internal/session/agent.go index b92b13733..19a4341d1 100644 --- a/monkeyai/backend/internal/session/agent.go +++ b/monkeyai/backend/internal/session/agent.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "log/slog" "net/http" "strings" @@ -219,6 +220,7 @@ func validateState(in sessionInput, row sessionRow) (bool, []byte, error) { in.Runtime = nil data, err := json.Marshal(in) if err != nil { + slog.Warn("操作失败", "module", "session", "operation", "validateState", "error", err) return false, nil, invalidState("会话快照无法规范化") } digest := sha256.Sum256(data) @@ -276,6 +278,7 @@ func turnHash(in turnInput, facts int32) ([]byte, error) { Turn turnInput `json:"turn"` }{FactsVersion: facts, Turn: in}) if err != nil { + slog.Warn("操作失败", "module", "session", "operation", "turnHash", "error", err) return nil, invalidState("轮次无法规范化") } digest := sha256.Sum256(data) diff --git a/monkeyai/backend/internal/session/decode.go b/monkeyai/backend/internal/session/decode.go index b303d0ff4..8bc0249aa 100644 --- a/monkeyai/backend/internal/session/decode.go +++ b/monkeyai/backend/internal/session/decode.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "io" + "log/slog" "net/http" "strings" "time" @@ -224,7 +225,11 @@ func decodeBody(w http.ResponseWriter, r *http.Request, limit int, target any) e } return resource.Invalid("gzip 请求格式无效") } - defer unzipped.Close() + defer func() { + if err := unzipped.Close(); err != nil { + slog.Error("关闭资源失败", "module", "session", "operation", "decodeBody", "error", err) + } + }() body = http.MaxBytesReader(w, unzipped, int64(decodedLimit)) } decoder := json.NewDecoder(body) @@ -240,6 +245,7 @@ func decodeBody(w http.ResponseWriter, r *http.Request, limit int, target any) e return resource.Invalid("请求必须为 JSON 对象") } if err := validateUniqueJSON(raw); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "session", "operation", "decodeBody", "error", err) return resource.Invalid("请求 JSON 字段重复或格式无效") } if err := decoder.Decode(new(json.RawMessage)); !errors.Is(err, io.EOF) { @@ -252,6 +258,7 @@ func decodeBody(w http.ResponseWriter, r *http.Request, limit int, target any) e return resource.Invalid("请求格式无效") } if err := json.Unmarshal(raw, target); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "session", "operation", "decodeBody", "error", err) return resource.Invalid("请求字段或格式无效") } return nil diff --git a/monkeyai/backend/internal/session/index.go b/monkeyai/backend/internal/session/index.go index 6fc232606..eac8b117f 100644 --- a/monkeyai/backend/internal/session/index.go +++ b/monkeyai/backend/internal/session/index.go @@ -3,6 +3,7 @@ package session import ( "context" "encoding/json" + "log/slog" "net/http" "time" @@ -70,6 +71,7 @@ func (s *Service) sessionIndex(w http.ResponseWriter, r *http.Request) { p, _ := identity.PrincipalFromContext(ctx) enabled, full, err := s.indexPolicy(ctx) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "session", "operation", "sessionIndex", "error", err) resource.Fail(r.Context(), w, sessionFailure(503, "authorization_state_unavailable", "摘要策略暂不可用")) return } @@ -135,6 +137,7 @@ func (s *Service) sessionIndex(w http.ResponseWriter, r *http.Request) { if s.runtime != nil { current.status, current.boot, current.epoch, err = s.runtime.RuntimeStatus(ctx, p.UserID, machine) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "session", "operation", "sessionIndex", "error", err) resource.Fail(r.Context(), w, sessionFailure(503, "authorization_state_unavailable", "设备状态暂不可用")) return } diff --git a/monkeyai/backend/internal/session/turns.go b/monkeyai/backend/internal/session/turns.go index c3ec262b6..5c1fe64be 100644 --- a/monkeyai/backend/internal/session/turns.go +++ b/monkeyai/backend/internal/session/turns.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "sort" "strconv" @@ -169,6 +170,9 @@ func parseAfterTurn(r *http.Request) (int32, error) { } value, err := strconv.ParseInt(values[0], 10, 32) if err != nil || value < 0 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "session", "operation", "parseAfterTurn", "error", err) + } return 0, invalidState("after_turn 无效") } return int32(value), nil @@ -541,13 +545,23 @@ func persistSnapshot(ctx context.Context, tx pgx.Tx, sessionID string, snapshot func sameSnapshotJSON(stored, expected []byte) bool { var left, right any if err := json.Unmarshal(stored, &left); err != nil { + slog.Warn("操作失败", "module", "session", "operation", "sameSnapshotJSON", "error", err) return false } if err := json.Unmarshal(expected, &right); err != nil { + slog.Warn("操作失败", "module", "session", "operation", "sameSnapshotJSON", "error", err) + return false + } + leftData, err := json.Marshal(left) + if err != nil { + slog.Error("编码会话状态失败", "error", err) + return false + } + rightData, err := json.Marshal(right) + if err != nil { + slog.Error("编码会话状态失败", "error", err) return false } - leftData, _ := json.Marshal(left) - rightData, _ := json.Marshal(right) return bytes.Equal(leftData, rightData) } diff --git a/monkeyai/backend/internal/setting/admin.go b/monkeyai/backend/internal/setting/admin.go index 89958af7f..d527d872f 100644 --- a/monkeyai/backend/internal/setting/admin.go +++ b/monkeyai/backend/internal/setting/admin.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/go-chi/chi/v5" ) @@ -46,6 +47,7 @@ func (s *Service) putSetting(w http.ResponseWriter, r *http.Request) { SchemaVersion int `json:"schema_version"` } if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<20)).Decode(&input); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "setting", "operation", "putSetting", "error", err) settingError(r.Context(), w, http.StatusBadRequest, "请求格式无效") return } diff --git a/monkeyai/backend/internal/setting/email.go b/monkeyai/backend/internal/setting/email.go index dfff0bef4..eebb9d9e7 100644 --- a/monkeyai/backend/internal/setting/email.go +++ b/monkeyai/backend/internal/setting/email.go @@ -7,7 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "mime" "net" @@ -18,6 +17,8 @@ import ( "strconv" "strings" "time" + + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" ) type emailConfig struct { @@ -33,6 +34,9 @@ type emailConfig struct { func (c emailConfig) validate() error { address, err := mail.ParseAddress(c.SenderEmail) if err != nil || address.Address != c.SenderEmail || strings.ContainsAny(c.SenderName, "\r\n") { + if err != nil { + slog.Warn("操作失败", "module", "setting", "operation", "validate", "error", err) + } return errors.New("发件人邮箱或名称无效") } if strings.TrimSpace(c.Host) == "" || strings.ContainsAny(c.Host, "\r\n /\\") || c.Port < 1 || c.Port > 65535 { @@ -62,6 +66,9 @@ func sendEmail(ctx context.Context, cfg emailConfig, to, subject, body string) e } recipient, err := mail.ParseAddress(to) if err != nil || recipient.Address != to { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "setting", "operation", "sendEmail", "error", err) + } return errors.New("收件邮箱无效") } ctx, cancel := context.WithTimeout(ctx, 15*time.Second) @@ -73,12 +80,12 @@ func sendEmail(ctx context.Context, cfg emailConfig, to, subject, body string) e } defer func() { if err := conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "setting.operation.warn", "关闭 SMTP 连接失败", "module", "setting", "operation", "sendEmail", "failure", smtpFailure(err)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "setting.operation.warn", "关闭 SMTP 连接失败", "module", "setting", "operation", "sendEmail", "error", err) } }() stop := context.AfterFunc(ctx, func() { if err := conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "setting.operation.warn", "取消 SMTP 连接时关闭失败", "module", "setting", "operation", "sendEmail", "failure", smtpFailure(err)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "setting.operation.warn", "取消 SMTP 连接时关闭失败", "module", "setting", "operation", "sendEmail", "error", err) } }) defer stop() @@ -101,7 +108,7 @@ func sendEmail(ctx context.Context, cfg emailConfig, to, subject, body string) e } defer func() { if err := client.Close(); err != nil && !errors.Is(err, net.ErrClosed) { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "setting.operation.warn", "关闭 SMTP 客户端失败", "module", "setting", "operation", "sendEmail", "failure", smtpFailure(err)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "setting.operation.warn", "关闭 SMTP 客户端失败", "module", "setting", "operation", "sendEmail", "error", err) } }() if cfg.Encryption == "starttls" { @@ -141,7 +148,7 @@ func sendEmail(ctx context.Context, cfg emailConfig, to, subject, body string) e } // DATA 已确认接收后,QUIT 失败不应误报发送失败。 if err := client.Quit(); err != nil && !errors.Is(err, net.ErrClosed) { - logging.Event(ctx, slog.Default(), slog.LevelWarn, "setting.operation.warn", "SMTP QUIT 失败", "module", "setting", "operation", "sendEmail", "failure", smtpFailure(err)) + logging.Event(ctx, slog.Default(), slog.LevelWarn, "setting.operation.warn", "SMTP QUIT 失败", "module", "setting", "operation", "sendEmail", "error", err) } return nil } @@ -157,30 +164,32 @@ func (s *Service) testEmail(w http.ResponseWriter, r *http.Request) { input.Recipient = strings.TrimSpace(input.Recipient) address, err := mail.ParseAddress(input.Recipient) if err != nil || address.Address != input.Recipient { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "setting", "operation", "testEmail", "error", err) + } settingError(r.Context(), w, 400, "收件邮箱无效") return } if err := s.Send(r.Context(), input.Recipient, "MonkeyAI 测试邮件", "这是一封 MonkeyAI 测试邮件,SMTP 发件配置已生效。"); err != nil { - logging.Event(r.Context(), slog.Default(), slog.LevelError, "setting.operation.error", "测试邮件发送失败", "module", "setting", "operation", "testEmail", "failure", smtpFailure(err)) + logging.Event(r.Context(), slog.Default(), slog.LevelError, "setting.operation.error", "测试邮件发送失败", "module", "setting", "operation", "testEmail", "error", err) settingError(r.Context(), w, http.StatusBadGateway, "邮件发送失败,请检查已保存的 SMTP 配置和服务连接") return } settingJSON(r.Context(), w, http.StatusOK, map[string]bool{"sent": true}) } -// SMTP 响应和网络错误可能回显收件人或认证信息,只记录协议状态及错误类别。 func smtpFailure(err error) []any { if response, ok := errors.AsType[*textproto.Error](err); ok { - return []any{"reason", "smtp_rejected", "smtp_status", response.Code} + return []any{"error", err, "reason", "smtp_rejected", "smtp_status", response.Code} } if errors.Is(err, ErrNotFound) { - return []any{"error", ErrNotFound} + return []any{"error", err} } if errors.Is(err, context.DeadlineExceeded) { - return []any{"reason", "timeout"} + return []any{"error", err, "reason", "timeout"} } if network, ok := errors.AsType[net.Error](err); ok { - return []any{"reason", "network_error", "timeout", network.Timeout()} + return []any{"error", err, "reason", "network_error", "timeout", network.Timeout()} } - return []any{"reason", "email_error", "error_type", fmt.Sprintf("%T", err)} + return []any{"error", err, "reason", "email_error"} } diff --git a/monkeyai/backend/internal/setting/email_test.go b/monkeyai/backend/internal/setting/email_test.go index 7245f30eb..2241123b8 100644 --- a/monkeyai/backend/internal/setting/email_test.go +++ b/monkeyai/backend/internal/setting/email_test.go @@ -149,9 +149,9 @@ func TestDefaultAuthenticationAndEmailSecret(t *testing.T) { } } -func TestSMTPFailureExcludesResponse(t *testing.T) { +func TestSMTPFailurePreservesResponse(t *testing.T) { logged := fmt.Sprint(smtpFailure(&textproto.Error{Code: 535, Msg: "secret@example.com private-token"})) - if strings.Contains(logged, "private-token") || strings.Contains(logged, "secret@example.com") || !strings.Contains(logged, "535") { + if !strings.Contains(logged, "private-token") || !strings.Contains(logged, "secret@example.com") || !strings.Contains(logged, "535") { t.Fatalf("SMTP 日志分类不安全: %s", logged) } } diff --git a/monkeyai/backend/internal/setting/proxy.go b/monkeyai/backend/internal/setting/proxy.go index 3e76b0dff..cfb59330a 100644 --- a/monkeyai/backend/internal/setting/proxy.go +++ b/monkeyai/backend/internal/setting/proxy.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/url" "strconv" "strings" @@ -21,16 +22,25 @@ type outboundProxy struct { func validateProxies(raw json.RawMessage) error { var proxies []outboundProxy if err := json.Unmarshal(raw, &proxies); err != nil || proxies == nil { + if err != nil { + slog.Warn("操作失败", "module", "setting", "operation", "validateProxies", "error", err) + } return errors.New("proxies 必须是数组") } seen := map[string]bool{} for _, proxy := range proxies { address, err := url.Parse(proxy.URL) if err != nil || (address.Scheme != "http" && address.Scheme != "https" && address.Scheme != "socks5") || address.Host == "" || address.User != nil || address.RawQuery != "" || address.Fragment != "" || address.Path != "" || address.Hostname() == "" || address.Port() == "" { + if err != nil { + slog.Warn("操作失败", "module", "setting", "operation", "validateProxies", "error", err) + } return errors.New("代理 URL 必须是 http、https 或 socks5 地址(含端口)") } port, err := strconv.Atoi(address.Port()) if err != nil || port < 1 || port > 65535 { + if err != nil { + slog.Warn("操作失败", "module", "setting", "operation", "validateProxies", "error", err) + } return errors.New("代理端口无效") } if proxy.ID == "" || proxy.Name == "" || seen[proxy.ID] || strings.TrimSpace(proxy.Name) != proxy.Name || (proxy.Password != "" && proxy.Username == "") { diff --git a/monkeyai/backend/internal/setting/service.go b/monkeyai/backend/internal/setting/service.go index 5e8692932..d70d2d5c0 100644 --- a/monkeyai/backend/internal/setting/service.go +++ b/monkeyai/backend/internal/setting/service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "slices" "time" @@ -79,6 +80,9 @@ func (s *Service) Put(ctx context.Context, key string, value json.RawMessage, sc } var object map[string]json.RawMessage if err := json.Unmarshal(value, &object); err != nil || object == nil { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "setting", "operation", "Put", "error", err) + } return Record{}, errors.New("value 必须是 JSON 对象") } if key == "authentication" { @@ -114,6 +118,9 @@ func (s *Service) Put(ctx context.Context, key string, value json.RawMessage, sc if key == "proxies" { var proxies []map[string]json.RawMessage if err := json.Unmarshal(object["proxies"], &proxies); err != nil || proxies == nil { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "setting", "operation", "Put", "error", err) + } return Record{}, errors.New("proxies 必须是数组") } for _, proxy := range proxies { @@ -121,17 +128,30 @@ func (s *Service) Put(ctx context.Context, key string, value json.RawMessage, sc return Record{}, errors.New("代理配置无效") } if rawString(proxy["id"]) == "" { - proxy["id"], _ = json.Marshal(resource.ID()) + encoded, encodeErr := json.Marshal(resource.ID()) + if encodeErr != nil { + return Record{}, encodeErr + } + proxy["id"] = encoded } } - object["proxies"], _ = json.Marshal(proxies) - value, _ = json.Marshal(object) + encoded, encodeErr := json.Marshal(proxies) + if encodeErr != nil { + return Record{}, encodeErr + } + object["proxies"] = encoded + encoded, encodeErr = json.Marshal(object) + if encodeErr != nil { + return Record{}, encodeErr + } + value = encoded } value, err := s.mergeSecrets(ctx, key, value) if err != nil { return Record{}, err } if err := json.Unmarshal(value, &object); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "setting", "operation", "Put", "error", err) return Record{}, errors.New("value 必须是 JSON 对象") } if err := validate(key, object); err != nil { @@ -280,6 +300,7 @@ func (s *Service) mergeSecrets(ctx context.Context, key string, value json.RawMe } var next, previous map[string]any if err := json.Unmarshal(value, &next); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "setting", "operation", "mergeSecrets", "error", err) return nil, errors.New("value 必须是 JSON 对象") } if err := json.Unmarshal(existing.Value, &previous); err != nil { @@ -377,6 +398,7 @@ func validate(key string, value map[string]json.RawMessage) error { case "email": data, err := json.Marshal(value) if err != nil { + slog.Warn("操作失败", "module", "setting", "operation", "validate", "error", err) return errors.New("邮件配置格式无效") } var config emailConfig @@ -400,6 +422,7 @@ func validate(key string, value map[string]json.RawMessage) error { func rawString(value json.RawMessage) string { var result string if err := json.Unmarshal(value, &result); err != nil { + slog.Warn("操作失败", "module", "setting", "operation", "rawString", "error", err) return "" } return result diff --git a/monkeyai/backend/internal/skill/package.go b/monkeyai/backend/internal/skill/package.go index a32251578..b9a386012 100644 --- a/monkeyai/backend/internal/skill/package.go +++ b/monkeyai/backend/internal/skill/package.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "fmt" "io" + "log/slog" "path" "regexp" "sort" @@ -35,6 +36,7 @@ func Parse(b []byte) (Package, error) { } r, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) if err != nil { + slog.Warn("操作失败", "module", "skill", "operation", "Parse", "error", err) return p, fmt.Errorf("无效 ZIP") } root := "" @@ -61,9 +63,14 @@ func Parse(b []byte) (Package, error) { return p, err } data, err := io.ReadAll(io.LimitReader(rd, int64(MaxExpanded-total+1))) - rd.Close() + if err := rd.Close(); err != nil { + slog.Error("关闭资源失败", "module", "skill", "operation", "Parse", "error", err) + } total += len(data) if err != nil || total > MaxExpanded { + if err != nil { + slog.Warn("操作失败", "module", "skill", "operation", "Parse", "error", err) + } return p, fmt.Errorf("技能包损坏或解包大小超限") } p.Files[name] = data @@ -99,6 +106,7 @@ func Parse(b []byte) (Package, error) { return p, fmt.Errorf("SKILL.md 元信息未闭合") } if err = yaml.Unmarshal([]byte(text[4:4+end]), &p.Front); err != nil { + slog.Warn("操作失败", "module", "skill", "operation", "Parse", "error", err) return p, fmt.Errorf("SKILL.md 元信息无效") } p.Name, _ = p.Front["name"].(string) diff --git a/monkeyai/backend/internal/skill/service.go b/monkeyai/backend/internal/skill/service.go index 45a6c4811..7be9744b5 100644 --- a/monkeyai/backend/internal/skill/service.go +++ b/monkeyai/backend/internal/skill/service.go @@ -4,12 +4,13 @@ import ( "context" "encoding/json" "fmt" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "io" "log/slog" "net/http" "strconv" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/identity" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/skill/sqlc" @@ -47,6 +48,9 @@ func (s *Service) read(ctx context.Context, object resource.Object) (Package, er } p, err := Parse(b) if err != nil || int64(len(b)) != object.Int("package_size_bytes") || p.SHA != object.String("package_sha256") { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "skill", "operation", "read", "error", err) + } return Package{}, &resource.Error{Status: 502, Code: "storage_corrupted", Message: "技能包大小或摘要不一致,请重新上传"} } return p, nil @@ -158,6 +162,7 @@ func (s *Service) manifest(w http.ResponseWriter, r *http.Request, personal bool func (s *Service) upload(w http.ResponseWriter, r *http.Request, personal bool) { r.Body = http.MaxBytesReader(w, r.Body, MaxPackage+(1<<20)) if err := r.ParseMultipartForm(1 << 20); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "skill", "operation", "upload", "error", err) resource.Fail(r.Context(), w, resource.Invalid("技能包上传无效或超限")) return } @@ -168,6 +173,7 @@ func (s *Service) upload(w http.ResponseWriter, r *http.Request, personal bool) }() f, _, err := r.FormFile("package") if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "skill", "operation", "upload", "error", err) resource.Fail(r.Context(), w, resource.Invalid("缺少 package")) return } @@ -189,6 +195,9 @@ func (s *Service) upload(w http.ResponseWriter, r *http.Request, personal bool) in := resource.Object{} if meta := r.FormValue("metadata"); meta != "" { if err = json.Unmarshal([]byte(meta), &in); err != nil || in == nil { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "skill", "operation", "upload", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("元数据格式无效")) return } diff --git a/monkeyai/backend/internal/stats/conversations.go b/monkeyai/backend/internal/stats/conversations.go index d8b53f3f7..1b6ddddd3 100644 --- a/monkeyai/backend/internal/stats/conversations.go +++ b/monkeyai/backend/internal/stats/conversations.go @@ -2,6 +2,7 @@ package stats import ( "context" + "log/slog" "net/http" "strconv" "strings" @@ -39,6 +40,9 @@ func parseConversationFilter(r *http.Request) (conversationFilter, error) { if f.params.TaskID != "" { var id pgtype.UUID if err := id.Scan(f.params.TaskID); err != nil || !id.Valid { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "parseConversationFilter", "error", err) + } return f, resource.Invalid("task_id 无效") } } @@ -52,6 +56,7 @@ func parseConversationFilter(r *http.Request) (conversationFilter, error) { if value := q.Get(field.name); value != "" { at, err := time.Parse(time.RFC3339, value) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "parseConversationFilter", "error", err) return f, resource.Invalid(field.name + " 需使用带时区的 RFC3339 时间") } *field.target = &at @@ -71,6 +76,9 @@ func parseConversationFilter(r *http.Request) (conversationFilter, error) { if value := q.Get(field.name); value != "" { n, err := strconv.Atoi(value) if err != nil || n < 1 || n > field.maximum { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "parseConversationFilter", "error", err) + } return f, resource.Invalid(field.message) } *field.target = n diff --git a/monkeyai/backend/internal/stats/history.go b/monkeyai/backend/internal/stats/history.go index f7a18ceac..3680d8e8a 100644 --- a/monkeyai/backend/internal/stats/history.go +++ b/monkeyai/backend/internal/stats/history.go @@ -2,6 +2,7 @@ package stats import ( "context" + "log/slog" "net/http" "strconv" "strings" @@ -9,7 +10,6 @@ import ( "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/stats/sqlc" - "github.com/jackc/pgx/v5" ) @@ -25,6 +25,7 @@ func (s *Service) history(w http.ResponseWriter, r *http.Request) { if value := q.Get(key); value != "" { at, err := time.Parse(time.RFC3339, value) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "history", "error", err) resource.Fail(r.Context(), w, resource.Invalid("筛选时间需使用带时区的 ISO 格式")) return } @@ -40,6 +41,9 @@ func (s *Service) history(w http.ResponseWriter, r *http.Request) { if value := q.Get(key); value != "" { n, err := strconv.Atoi(value) if err != nil || n < 1 || n > 1_000_000 || (key == "page_size" && n > 500) { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "history", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("分页参数无效")) return } diff --git a/monkeyai/backend/internal/stats/reporting_detail.go b/monkeyai/backend/internal/stats/reporting_detail.go index ee6957d6b..bc683fb79 100644 --- a/monkeyai/backend/internal/stats/reporting_detail.go +++ b/monkeyai/backend/internal/stats/reporting_detail.go @@ -2,6 +2,7 @@ package stats import ( "context" + "log/slog" "net/http" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/resource" @@ -15,6 +16,9 @@ func (s *Service) reportingDetail(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var uuid pgtype.UUID if err := uuid.Scan(id); err != nil || !uuid.Valid { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "reportingDetail", "error", err) + } w.Header().Set("Cache-Control", "private, no-store") resource.Fail(r.Context(), w, resource.Invalid("会话 ID 无效")) return diff --git a/monkeyai/backend/internal/stats/reporting_filter.go b/monkeyai/backend/internal/stats/reporting_filter.go index 621866220..ec038e24a 100644 --- a/monkeyai/backend/internal/stats/reporting_filter.go +++ b/monkeyai/backend/internal/stats/reporting_filter.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "log/slog" "net/http" "net/url" "strconv" @@ -36,6 +37,7 @@ func parseReportingFilter(r *http.Request, now time.Time, maxLimit int) (reporti } at, err := time.Parse(time.RFC3339, q.Get(key)) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "parseReportingFilter", "error", err) return f, resource.Invalid(key + " 需使用 RFC3339 时间") } if key == "from" { @@ -47,6 +49,9 @@ func parseReportingFilter(r *http.Request, now time.Time, maxLimit int) (reporti if q.Has("limit") { n, err := strconv.Atoi(q.Get("limit")) if err != nil || n < 1 || n > maxLimit { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "parseReportingFilter", "error", err) + } return f, resource.Invalid("limit 必须为 1 至 " + strconv.Itoa(maxLimit)) } f.limit = n @@ -64,6 +69,9 @@ func parseReportingFilter(r *http.Request, now time.Time, maxLimit int) (reporti if *item.target != "" { var id pgtype.UUID if err := id.Scan(*item.target); err != nil || !id.Valid { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "parseReportingFilter", "error", err) + } return f, resource.Invalid(item.name + " 无效") } } @@ -96,6 +104,7 @@ func parseReportingFilter(r *http.Request, now time.Time, maxLimit int) (reporti if q.Has(item.name) { value, err := strconv.ParseBool(q.Get(item.name)) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "parseReportingFilter", "error", err) return f, resource.Invalid(item.name + " 无效") } *item.target = value @@ -109,17 +118,22 @@ func parseReportingFilter(r *http.Request, now time.Time, maxLimit int) (reporti var c reportCursor raw, err := base64.RawURLEncoding.DecodeString(f.cursor) if err != nil || json.Unmarshal(raw, &c) != nil { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "parseReportingFilter", "error", err) + } return f, resource.Invalid("cursor 无效") } if !q.Has("from") { f.from, err = time.Parse(time.RFC3339Nano, c.From) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "parseReportingFilter", "error", err) return f, resource.Invalid("cursor 时间无效") } } if !q.Has("until") { f.until, err = time.Parse(time.RFC3339Nano, c.Until) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "stats", "operation", "parseReportingFilter", "error", err) return f, resource.Invalid("cursor 时间无效") } } @@ -169,7 +183,11 @@ func (f reportingFilter) sqlParams(c reportCursor, sessionID string) []byte { "outcome": optional(f.outcome), "include_placeholders": f.placeholders, "include_purged": f.purged, "cursor_key": optional(c.Key), "session_id": optional(sessionID), } - data, _ := json.Marshal(values) + data, err := json.Marshal(values) + if err != nil { + slog.Error("编码统计筛选条件失败", "error", err) + return nil + } return data } @@ -182,6 +200,9 @@ func (f reportingFilter) decodeCursor() (reportCursor, error) { var c reportCursor raw, err := base64.RawURLEncoding.DecodeString(f.cursor) if err != nil || json.Unmarshal(raw, &c) != nil || c.Fingerprint != f.fingerprint || c.From != f.from.Format(time.RFC3339Nano) || c.Until != f.until.Format(time.RFC3339Nano) { + if err != nil { + slog.Warn("操作失败", "module", "stats", "operation", "decodeCursor", "error", err) + } return c, resource.Invalid("cursor 无效或筛选条件已变化") } return c, nil @@ -189,7 +210,11 @@ func (f reportingFilter) decodeCursor() (reportCursor, error) { func (f reportingFilter) encodeCursor(c reportCursor) string { c.Fingerprint = f.fingerprint c.From, c.Until = f.from.Format(time.RFC3339Nano), f.until.Format(time.RFC3339Nano) - raw, _ := json.Marshal(c) + raw, err := json.Marshal(c) + if err != nil { + slog.Error("编码统计游标失败", "error", err) + return "" + } return base64.RawURLEncoding.EncodeToString(raw) } diff --git a/monkeyai/backend/internal/testmember/member.go b/monkeyai/backend/internal/testmember/member.go index e92596e64..77f7356b8 100644 --- a/monkeyai/backend/internal/testmember/member.go +++ b/monkeyai/backend/internal/testmember/member.go @@ -3,6 +3,7 @@ package testmember import ( "context" "errors" + "log/slog" "time" groupdb "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/group/sqlc" @@ -26,7 +27,11 @@ func (w userWriter) EnsureInitialAdmin(ctx context.Context, in member.InitialAdm if err != nil { return err } - defer tx.Rollback(context.WithoutCancel(ctx)) + defer func() { + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "testmember", "operation", "EnsureInitialAdmin", "error", err) + } + }() q := sqlc.New(tx) if _, err = q.LockInitialAdmin(ctx); err != nil { return err @@ -53,7 +58,11 @@ func (w userWriter) CreateUser(ctx context.Context, in member.CreateUser) (membe if err != nil { return member.User{}, err } - defer tx.Rollback(context.WithoutCancel(ctx)) + defer func() { + if err := tx.Rollback(context.WithoutCancel(ctx)); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "testmember", "operation", "CreateUser", "error", err) + } + }() q := sqlc.New(tx) if err = q.LockUserCreationGroups(ctx); err != nil { return member.User{}, err diff --git a/monkeyai/backend/internal/videogen/cleanup.go b/monkeyai/backend/internal/videogen/cleanup.go index 3ef1a3802..d98cad400 100644 --- a/monkeyai/backend/internal/videogen/cleanup.go +++ b/monkeyai/backend/internal/videogen/cleanup.go @@ -2,8 +2,11 @@ package videogen import ( "context" + "errors" + "log/slog" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/videogen/sqlc" + "github.com/jackc/pgx/v5" ) func (s *Service) Cleanup(ctx context.Context) error { @@ -26,7 +29,9 @@ func (s *Service) Cleanup(ctx context.Context) error { err = q.RemoveExpiredVideoInput(ctx, input.ID) } if err != nil { - _ = transaction.Rollback(ctx) + if err := transaction.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.Error("回滚事务失败", "module", "videogen", "operation", "Cleanup", "error", err) + } return err } if err := transaction.Commit(ctx); err != nil { diff --git a/monkeyai/backend/internal/videogen/diagnostics_test.go b/monkeyai/backend/internal/videogen/diagnostics_test.go index a1f53c985..b8fdd551e 100644 --- a/monkeyai/backend/internal/videogen/diagnostics_test.go +++ b/monkeyai/backend/internal/videogen/diagnostics_test.go @@ -92,7 +92,8 @@ func TestOutputFailureDiagnostics(t *testing.T) { Code string `json:"error_code"` } `json:"error"` } - if err := json.Unmarshal(logs.Bytes(), &row); err != nil { + lines := bytes.Split(bytes.TrimSpace(logs.Bytes()), []byte("\n")) + if err := json.Unmarshal(lines[len(lines)-1], &row); err != nil { t.Fatal(err) } if row.Phase != "save_output" || !row.Proxy || row.Output.Phase != tc.phase || row.Error.Code != tc.code { diff --git a/monkeyai/backend/internal/videogen/http.go b/monkeyai/backend/internal/videogen/http.go index b0860430c..4f54835ec 100644 --- a/monkeyai/backend/internal/videogen/http.go +++ b/monkeyai/backend/internal/videogen/http.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "io" + "log/slog" "math/big" "net" "net/http" @@ -59,6 +60,9 @@ func providerClient() *http.Client { } ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host) if err != nil || len(ips) == 0 { + if err != nil { + slog.Warn("操作失败", "module", "videogen", "operation", "providerClient", "error", err) + } return nil, errResolveFailed } for _, ip := range ips { @@ -84,10 +88,16 @@ func providerClient() *http.Client { func providerURL(base, endpoint string) (string, error) { u, err := url.Parse(base) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" || u.User != nil || u.Fragment != "" || u.RawQuery != "" { + if err != nil { + slog.Warn("操作失败", "module", "videogen", "operation", "providerURL", "error", err) + } return "", errUpstreamURL } suffix, err := url.Parse(endpoint) if err != nil || suffix.IsAbs() || suffix.Host != "" || suffix.User != nil || suffix.Fragment != "" { + if err != nil { + slog.Warn("操作失败", "module", "videogen", "operation", "providerURL", "error", err) + } return "", errUpstreamEndpoint } u.Path = strings.TrimRight(u.Path, "/") + "/" + strings.TrimLeft(suffix.Path, "/") @@ -104,6 +114,9 @@ func providerJSON(ctx context.Context, client *http.Client, method, base, endpoi if payload != nil { encoded, err := json.Marshal(payload) if err != nil || len(encoded) > 40<<20 { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "videogen", "operation", "providerJSON", "error", err) + } return nil, 0, errors.New("视频上游请求超过大小限制") } body = bytes.NewReader(encoded) @@ -120,9 +133,16 @@ func providerJSON(ctx context.Context, client *http.Client, method, base, endpoi if err != nil { return nil, 0, err } - defer response.Body.Close() + defer func() { + if err := response.Body.Close(); err != nil { + slog.Error("关闭资源失败", "module", "videogen", "operation", "providerJSON", "error", err) + } + }() content, err := io.ReadAll(io.LimitReader(response.Body, maxProviderResponse+1)) if err != nil || len(content) > maxProviderResponse { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "videogen", "operation", "providerJSON", "error", err) + } return nil, 0, errors.New("视频上游响应过大或读取失败") } return content, response.StatusCode, nil @@ -152,6 +172,9 @@ func downloadClient(client *http.Client) *http.Client { func providerMedia(ctx context.Context, client *http.Client, address string) (io.ReadCloser, int64, error) { u, err := url.Parse(address) if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.Fragment != "" { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "videogen", "operation", "providerMedia", "error", err) + } return nil, 0, errOutputURL } client = downloadClient(client) @@ -173,7 +196,9 @@ func providerMedia(ctx context.Context, client *http.Client, address string) (io invalid = errOutputEmpty } if invalid.code != "" { - response.Body.Close() + if err := response.Body.Close(); err != nil { + slog.Error("关闭资源失败", "module", "videogen", "operation", "providerMedia", "error", err) + } return nil, 0, &mediaResponseError{diagnosticError: invalid, status: response.StatusCode, expected: response.ContentLength} } return response.Body, response.ContentLength, nil diff --git a/monkeyai/backend/internal/videogen/inputs.go b/monkeyai/backend/internal/videogen/inputs.go index d44fae26b..8a4b8f258 100644 --- a/monkeyai/backend/internal/videogen/inputs.go +++ b/monkeyai/backend/internal/videogen/inputs.go @@ -8,7 +8,10 @@ import ( "errors" "fmt" "image" + "log/slog" + _ "image/jpeg" + _ "image/png" "io" "math/big" @@ -40,6 +43,9 @@ func (s *Service) Upload(ctx context.Context, userID string, data []byte) (Input } config, format, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil || config.Width < 1 || config.Height < 1 || int64(config.Width)*int64(config.Height) > maxInputPixels { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "videogen", "operation", "Upload", "error", err) + } return InputFile{}, resource.Invalid("视频参考图像素无效") } mime := "" @@ -66,7 +72,9 @@ func (s *Service) Upload(ctx context.Context, userID string, data []byte) (Input if err != nil { cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) defer cancel() - _ = s.storage.Delete(cleanup, key) + if err := s.storage.Delete(cleanup, key); err != nil { + slog.Error("清理存储对象失败", "module", "videogen", "operation", "Upload", "error", err) + } return InputFile{}, err } return InputFile{FileID: id, MIMEType: mime, Width: config.Width, Height: config.Height, ExpiresAt: expiry}, nil @@ -97,9 +105,16 @@ func (s *Service) loadInput(ctx context.Context, userID string, ref Reference, s if err != nil { return Reference{}, err } - defer body.Close() + defer func() { + if err := body.Close(); err != nil { + slog.Error("关闭资源失败", "module", "videogen", "operation", "loadInput", "error", err) + } + }() data, err := io.ReadAll(io.LimitReader(body, maxInputBytes+1)) if err != nil || len(data) != int(row.ByteSize) { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "videogen", "operation", "loadInput", "error", err) + } return Reference{}, errors.New("参考图读取或大小校验失败") } digest := sha256.Sum256(data) diff --git a/monkeyai/backend/internal/videogen/outputs.go b/monkeyai/backend/internal/videogen/outputs.go index 9cf38bfb1..79b198243 100644 --- a/monkeyai/backend/internal/videogen/outputs.go +++ b/monkeyai/backend/internal/videogen/outputs.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "os" "time" @@ -54,15 +55,27 @@ func (s *Service) saveOutput(ctx context.Context, client *http.Client, jobID, ad if err != nil { return VideoOutput{}, err } - defer stream.Close() + defer func() { + if err := stream.Close(); err != nil { + slog.Error("关闭资源失败", "module", "videogen", "operation", "saveOutput", "error", err) + } + }() failure.expected = expected failure.phase = "create_temp" file, err := os.CreateTemp("", "monkeyai-video-*.mp4") if err != nil { return VideoOutput{}, err } - defer os.Remove(file.Name()) - defer file.Close() + defer func() { + if err := os.Remove(file.Name()); err != nil && !os.IsNotExist(err) { + slog.Error("清理临时文件失败", "module", "videogen", "operation", "saveOutput", "error", err) + } + }() + defer func() { + if err := file.Close(); err != nil { + slog.Error("关闭资源失败", "module", "videogen", "operation", "saveOutput", "error", err) + } + }() failure.phase = "download_body" count, err := io.Copy(file, io.LimitReader(stream, maxOutputBytes+1)) failure.received = count @@ -81,6 +94,7 @@ func (s *Service) saveOutput(ctx context.Context, client *http.Client, jobID, ad failure.phase = "inspect_media" info, err := inspectMP4(file, count) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "videogen", "operation", "saveOutput", "error", err) info = mediaInfo{DurationMs: upstreamDurationMs} } failure.phase = "hash_output" @@ -114,7 +128,9 @@ func (s *Service) saveOutput(ctx context.Context, client *http.Client, jobID, ad if err != nil { cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) defer cancel() - _ = s.storage.Delete(cleanup, key) + if err := s.storage.Delete(cleanup, key); err != nil { + slog.Error("清理存储对象失败", "module", "videogen", "operation", "saveOutput", "error", err) + } return VideoOutput{}, err } return VideoOutput{FileID: id, URL: "/v1/videos/outputs/" + id, MIMEType: "video/mp4", Width: info.Width, Height: info.Height, diff --git a/monkeyai/backend/internal/videogen/provider.go b/monkeyai/backend/internal/videogen/provider.go index 7a7120083..cda3ef6eb 100644 --- a/monkeyai/backend/internal/videogen/provider.go +++ b/monkeyai/backend/internal/videogen/provider.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "net/url" "strconv" @@ -90,6 +91,7 @@ func submitProvider(ctx context.Context, client *http.Client, target proxy.Targe endpoint = "/contents/generations/tasks" var audio bool if err := json.Unmarshal(input.Params["generate_audio"], &audio); err != nil { + slog.WarnContext(ctx, "操作失败", "module", "videogen", "operation", "submitProvider", "error", err) return "", errors.New("视频音频开关无效") } request["generate_audio"] = audio @@ -254,6 +256,9 @@ func queryProvider(ctx context.Context, client *http.Client, target proxy.Target result.Status = "succeeded" result.DurationMs, err = durationMillis(value, maxSeconds) if err != nil || result.URL == "" { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "videogen", "operation", "queryProvider", "error", err) + } return providerResult{}, errors.New("视频供应商未提供可验证的结果") } case "failed", "expired", "cancelled": diff --git a/monkeyai/backend/internal/videogen/service.go b/monkeyai/backend/internal/videogen/service.go index 8c48b5bfd..54284c0ce 100644 --- a/monkeyai/backend/internal/videogen/service.go +++ b/monkeyai/backend/internal/videogen/service.go @@ -6,13 +6,14 @@ import ( "encoding/hex" "encoding/json" "errors" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "log/slog" "net/http" "strings" "sync" "time" + "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" + "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/billing" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/model" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/proxy" @@ -94,6 +95,9 @@ func (s *Service) Submit(ctx context.Context, target proxy.Target, input Generat } cap, err := model.VideoCapabilitiesFor(item.Provider, item.ModelID) if err != nil || model.ValidateVideoCapabilities(item, cap) != nil { + if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "videogen", "operation", "Submit", "error", err) + } return Task{}, resource.Invalid("视频模型能力不可用") } values, err := model.NormalizeVideoParams(item.VideoConfig, cap, input.Mode, input.Params) @@ -173,14 +177,21 @@ func (s *Service) Submit(ctx context.Context, target proxy.Target, input Generat }) if err != nil { if key != "" { - if existing, _ := s.idempotent(ctx, target.UserID, key, requestHash); existing != "" { + existing, lookupErr := s.idempotent(ctx, target.UserID, key, requestHash) + if lookupErr != nil { + return Task{}, errors.Join(err, lookupErr) + } + if existing != "" { return s.GetTask(ctx, target.UserID, existing) } } return Task{}, err } jobID := resource.ID() - priceJSON, _ := json.Marshal(price) + priceJSON, err := json.Marshal(price) + if err != nil { + return Task{}, errors.Join(err, s.billing.Release(context.WithoutCancel(ctx), reservation.ID, "task_encode_failed")) + } err = sqlc.New(s.pool).InsertVideoJob(ctx, sqlc.InsertVideoJobParams{ ID: jobID, UserID: target.UserID, ModelID: item.ID, SessionID: target.SessionID, Provider: string(item.Provider), Mode: string(input.Mode), RequestHash: requestHash, @@ -188,7 +199,9 @@ func (s *Service) Submit(ctx context.Context, target proxy.Target, input Generat BillingTransactionID: reservation.ID, }) if err != nil { - _ = s.billing.Release(context.WithoutCancel(ctx), reservation.ID, "task_create_failed") + if err := s.billing.Release(context.WithoutCancel(ctx), reservation.ID, "task_create_failed"); err != nil { + slog.Error("释放计费预留失败", "module", "videogen", "operation", "Submit", "error", err) + } return Task{}, err } for ordinal, ref := range loaded { @@ -320,16 +333,21 @@ func (s *Service) fail(ctx context.Context, id, transactionID, reason string) { if err := s.billing.Release(ctx, transactionID, reason); err != nil { logging.Event(ctx, slog.Default(), slog.LevelError, "videogen.operation.error", "释放视频计费预留失败", "module", "videogen", "operation", "fail", "job_id", id, "error", err) code := "billing_unknown" - _ = sqlc.New(s.pool).MarkVideoJobUnknown(ctx, sqlc.MarkVideoJobUnknownParams{ID: id, ErrorCode: &code}) + if err := sqlc.New(s.pool).MarkVideoJobUnknown(ctx, sqlc.MarkVideoJobUnknownParams{ID: id, ErrorCode: &code}); err != nil { + slog.Error("更新视频任务状态失败", "module", "videogen", "operation", "fail", "error", err) + } return } - _ = sqlc.New(s.pool).MarkVideoJobFailed(ctx, sqlc.MarkVideoJobFailedParams{ID: id, ErrorCode: &reason}) + if err := sqlc.New(s.pool).MarkVideoJobFailed(ctx, sqlc.MarkVideoJobFailedParams{ID: id, ErrorCode: &reason}); err != nil { + slog.Error("更新视频任务状态失败", "module", "videogen", "operation", "fail", "error", err) + } } func (s *Service) dispatch(ctx context.Context, target proxy.Target, jobID, reservationID string, input GenerateInput) { ctx, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() if err := s.billing.Start(ctx, reservationID); err != nil { + logging.Diagnostic(ctx, slog.Default(), slog.LevelError, "billing.start.failed", err, slog.String("job_id", jobID), slog.String("transaction_id", reservationID)) s.fail(ctx, jobID, reservationID, "billing_start_failed") return } @@ -337,19 +355,27 @@ func (s *Service) dispatch(ctx context.Context, target proxy.Target, jobID, rese if err != nil { var rejected providerRejected if errors.As(err, &rejected) { + logging.Diagnostic(ctx, slog.Default(), slog.LevelWarn, "video.provider.rejected", err, slog.String("job_id", jobID)) s.fail(ctx, jobID, reservationID, "provider_rejected") return } logging.Event(ctx, slog.Default(), slog.LevelWarn, "videogen.operation.warn", "视频上游受理结果未知", "module", "videogen", "operation", "dispatch", "job_id", jobID, "error", err) code := "provider_submit_unknown" - _ = sqlc.New(s.pool).MarkVideoJobUnknown(ctx, sqlc.MarkVideoJobUnknownParams{ID: jobID, ErrorCode: &code}) + if err := sqlc.New(s.pool).MarkVideoJobUnknown(ctx, sqlc.MarkVideoJobUnknownParams{ID: jobID, ErrorCode: &code}); err != nil { + slog.Error("更新视频任务状态失败", "module", "videogen", "operation", "dispatch", "error", err) + } return } if affected, e := sqlc.New(s.pool).MarkVideoJobSubmitted(ctx, sqlc.MarkVideoJobSubmittedParams{ID: jobID, ProviderJobID: &providerID}); e != nil || affected != 1 { err = e + if err == nil { + err = errors.New("视频任务状态已变化,未登记上游任务") + } logging.Event(ctx, slog.Default(), slog.LevelError, "videogen.operation.error", "登记视频上游任务失败", "module", "videogen", "operation", "dispatch", "job_id", jobID, "error", err) code := "provider_job_unknown" - _ = sqlc.New(s.pool).MarkVideoJobUnknown(ctx, sqlc.MarkVideoJobUnknownParams{ID: jobID, ErrorCode: &code}) + if err := sqlc.New(s.pool).MarkVideoJobUnknown(ctx, sqlc.MarkVideoJobUnknownParams{ID: jobID, ErrorCode: &code}); err != nil { + slog.Error("更新视频任务状态失败", "module", "videogen", "operation", "dispatch", "error", err) + } } } diff --git a/monkeyai/backend/internal/videoproxy/proxy.go b/monkeyai/backend/internal/videoproxy/proxy.go index 909d66a83..b67a3faa0 100644 --- a/monkeyai/backend/internal/videoproxy/proxy.go +++ b/monkeyai/backend/internal/videoproxy/proxy.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "strconv" "strings" @@ -54,6 +55,9 @@ func (p *Proxy) authorize(w http.ResponseWriter, r *http.Request) (string, strin } userID, err := p.keys.Authenticate(r.Context(), credential, "model:invoke") if err != nil || userID == "" { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "videoproxy", "operation", "authorize", "error", err) + } resource.Fail(r.Context(), w, &resource.Error{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "API Key 无效"}) return "", "", false } @@ -68,16 +72,23 @@ func (p *Proxy) upload(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 11<<20) parts, err := r.MultipartReader() if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "videoproxy", "operation", "upload", "error", err) resource.Fail(r.Context(), w, resource.Invalid("参考图上传格式无效")) return } part, err := parts.NextPart() if err != nil || part.FormName() != "file" { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "videoproxy", "operation", "upload", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("缺少参考图 file 字段")) return } data, err := io.ReadAll(io.LimitReader(part, (10<<20)+1)) if err != nil || len(data) == 0 || len(data) > 10<<20 { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "videoproxy", "operation", "upload", "error", err) + } resource.Fail(r.Context(), w, &resource.Error{Status: http.StatusRequestEntityTooLarge, Code: "video_input_too_large", Message: "视频参考图超过大小限制"}) return } @@ -106,6 +117,9 @@ func (p *Proxy) generate(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(&input); err != nil || input.Model == "" || input.Mode == "" { + if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "videoproxy", "operation", "generate", "error", err) + } resource.Fail(r.Context(), w, resource.Invalid("视频生成请求格式无效")) return } @@ -172,30 +186,40 @@ func (p *Proxy) output(w http.ResponseWriter, r *http.Request) { resource.Fail(r.Context(), w, err) return } - defer body.Close() + defer func() { + if err := body.Close(); err != nil { + slog.Error("关闭资源失败", "module", "videoproxy", "operation", "output", "error", err) + } + }() w.Header().Set("Cache-Control", "private, no-store") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Type", mime) w.Header().Set("Accept-Ranges", "bytes") start, end, ranged, err := parseRange(r.Header.Get("Range"), size) if err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "videoproxy", "operation", "output", "error", err) w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", size)) w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) return } if ranged { if _, err := io.CopyN(io.Discard, body, start); err != nil { + slog.WarnContext(r.Context(), "操作失败", "module", "videoproxy", "operation", "output", "error", err) return } w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size)) w.Header().Set("Content-Length", strconv.FormatInt(end-start+1, 10)) w.WriteHeader(http.StatusPartialContent) - _, _ = io.CopyN(w, body, end-start+1) + if _, err := io.CopyN(w, body, end-start+1); err != nil { + slog.Error("传输响应失败", "module", "videoproxy", "operation", "output", "error", err) + } return } w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) w.WriteHeader(http.StatusOK) - _, _ = io.CopyN(w, body, size) + if _, err := io.CopyN(w, body, size); err != nil { + slog.Error("传输响应失败", "module", "videoproxy", "operation", "output", "error", err) + } } func parseRange(header string, size int64) (int64, int64, bool, error) { @@ -212,6 +236,9 @@ func parseRange(header string, size int64) (int64, int64, bool, error) { if parts[0] == "" { length, err := strconv.ParseInt(parts[1], 10, 64) if err != nil || length <= 0 { + if err != nil { + slog.Warn("操作失败", "module", "videoproxy", "operation", "parseRange", "error", err) + } return 0, 0, false, errors.New("视频 Range 长度无效") } if length > size { @@ -221,12 +248,18 @@ func parseRange(header string, size int64) (int64, int64, bool, error) { } start, err := strconv.ParseInt(parts[0], 10, 64) if err != nil || start >= size || start < 0 { + if err != nil { + slog.Warn("操作失败", "module", "videoproxy", "operation", "parseRange", "error", err) + } return 0, 0, false, errors.New("视频 Range 起点无效") } end := size - 1 if parts[1] != "" { end, err = strconv.ParseInt(parts[1], 10, 64) if err != nil || end < start { + if err != nil { + slog.Warn("操作失败", "module", "videoproxy", "operation", "parseRange", "error", err) + } return 0, 0, false, errors.New("视频 Range 终点无效") } if end >= size { diff --git a/monkeyai/backend/logging/benchmark_test.go b/monkeyai/backend/logging/benchmark_test.go index 32da0805d..ee2b583ac 100644 --- a/monkeyai/backend/logging/benchmark_test.go +++ b/monkeyai/backend/logging/benchmark_test.go @@ -9,12 +9,8 @@ import ( ) func BenchmarkHTTPStreaming(b *testing.B) { - r, err := NewRuntime(io.Discard, slog.LevelInfo, DefaultSettings()) - if err != nil { - b.Fatal(err) - } - defer r.Close() - handler := HTTP(r.Logger())(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger := New(io.Discard, slog.LevelInfo) + handler := HTTP(logger)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") for i := 0; i < 10; i++ { _, _ = io.WriteString(w, "data: hello\n\n") diff --git a/monkeyai/backend/logging/context.go b/monkeyai/backend/logging/context.go index 471b8ca5c..458ef1d0b 100644 --- a/monkeyai/backend/logging/context.go +++ b/monkeyai/backend/logging/context.go @@ -122,11 +122,9 @@ func Event(ctx context.Context, logger *slog.Logger, level slog.Level, event, me runtime.Callers(2, pc[:]) clean := slog.NewRecord(record.Time, level, message, pc[0]) clean.AddAttrs(mergeAttrs(attrs)...) - _ = logger.Handler().Handle(ctx, clean) + handle(ctx, logger, clean) } -// Diagnostic records ownership before level filtering or deduplication. This -// prevents HTTP fallback errors from bypassing the primary event's policy. func syncRoute(ctx context.Context) { if state, ok := ctx.Value(contextKey{}).(*observation); ok { state.mu.Lock() @@ -161,7 +159,7 @@ func Diagnostic(ctx context.Context, logger *slog.Logger, level slog.Level, even } } record.AddAttrs(mergeAttrs(clean)...) - _ = logger.Handler().Handle(ctx, record) + handle(ctx, logger, record) } func contextAttrs(ctx context.Context) []slog.Attr { diff --git a/monkeyai/backend/logging/dedup.go b/monkeyai/backend/logging/dedup.go deleted file mode 100644 index 481bc845c..000000000 --- a/monkeyai/backend/logging/dedup.go +++ /dev/null @@ -1,343 +0,0 @@ -package logging - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "io" - "log/slog" - "os" - "sync" - "time" -) - -type Settings struct { - Enabled bool - Window time.Duration - MaxKeys int -} - -func DefaultSettings() Settings { return Settings{Enabled: true, Window: time.Minute, MaxKeys: 4096} } -func (s Settings) Validate() error { - if s.Window < time.Second || s.Window > 10*time.Minute || s.MaxKeys < 128 || s.MaxKeys > 65536 { - return errors.New("日志去重窗口或容量超出允许范围") - } - return nil -} - -type Snapshot struct { - Candidates, Details, Suppressed, Summaries, Overflow, OutputErrors uint64 - Calls, FailedCalls uint64 - Requests, FailedRequests uint64 - ActiveKeys int -} - -type entry struct { - key string - fields []slog.Attr - level slog.Level - event, fingerprint string - start, end, first, last, idle time.Time - window time.Duration - count, details uint64 - samples []string - sampleKey string - idleRound bool -} - -type Runtime struct { - mu sync.Mutex - settings Settings - output slog.Handler - logger *slog.Logger - entries map[string]*entry - stats Snapshot - now func() time.Time - stop, done chan struct{} - closed bool - closeOnce sync.Once - lastWarning time.Time -} - -// NewRuntime creates one scheduler and state container shared by all derived -// loggers. Its owner must close it after business workers and final events. -func NewRuntime(output io.Writer, level slog.Leveler, settings Settings) (*Runtime, error) { - if err := settings.Validate(); err != nil { - return nil, err - } - r := &Runtime{settings: settings, entries: make(map[string]*entry), now: time.Now, - stop: make(chan struct{}), done: make(chan struct{})} - r.output = jsonHandler(output, level) - r.logger = slog.New(&safeHandler{next: r.output, runtime: r}) - go func() { - defer close(r.done) - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - for { - select { - case <-r.stop: - return - case <-ticker.C: - r.Flush() - } - } - }() - return r, nil -} - -func CountCall(logger *slog.Logger, result string) { - if r := runtimeFor(logger); r != nil { - r.mu.Lock() - r.stats.Calls++ - if result == "failed" { - r.stats.FailedCalls++ - } - r.mu.Unlock() - } -} - -func (r *Runtime) Logger() *slog.Logger { return r.logger } -func (r *Runtime) Snapshot() Snapshot { - r.mu.Lock() - defer r.mu.Unlock() - s := r.stats - s.ActiveKeys = len(r.entries) - return s -} - -func (r *Runtime) emit(ctx context.Context, record slog.Record) error { - err := r.output.Handle(ctx, record) - if err != nil { - r.mu.Lock() - r.stats.OutputErrors++ - now := r.now() - warn := r.lastWarning.IsZero() || now.Sub(r.lastWarning) >= time.Minute - if warn { - r.lastWarning = now - } - r.mu.Unlock() - if warn { - _, _ = io.WriteString(os.Stderr, "结构化日志输出失败\n") - } - } - return err -} - -func (r *Runtime) submit(ctx context.Context, record slog.Record) error { - fields := scalars(record) - event := fields["event"].Value.String() - policy, registered := policies[event] - key := "" - var safeFields []slog.Attr - if registered && r.settings.Enabled { - key, safeFields = fingerprint(record.Level, fields, policy) - } - r.mu.Lock() - r.stats.Candidates++ - if !r.settings.Enabled || !registered || key == "" || r.closed { - r.stats.Details++ - r.mu.Unlock() - return r.emit(ctx, record) - } - now := r.now() - var pending []slog.Record - e := r.entries[key] - if e != nil && !now.Before(e.end) { - empty := e.count == 0 - if summary, ok := r.settle(e, "window"); ok { - pending = append(pending, summary) - } - if empty || now.Sub(e.end) >= e.window { - e.idleRound = true - e.start, e.end = now, now.Add(e.window) - } else { - e.start = e.end - e.end = e.start.Add(e.window) - } - } - if e == nil { - if len(r.entries) >= r.settings.MaxKeys { - for k, old := range r.entries { - ttl := max(10*time.Minute, 2*old.window) - if old.count == 0 && now.Sub(old.idle) >= ttl { - delete(r.entries, k) - } - } - } - if len(r.entries) >= r.settings.MaxKeys { - r.stats.Overflow++ - overflow := r.stats.Overflow - r.stats.Details++ - warn := r.lastWarning.IsZero() || now.Sub(r.lastWarning) >= time.Minute - if warn { - r.lastWarning = now - } - r.mu.Unlock() - for _, p := range pending { - _ = r.emit(ctx, p) - } - if warn { - warning := slog.NewRecord(now, slog.LevelWarn, "日志去重容量已满", 0) - warning.AddAttrs(slog.String("event", "logging.capacity.exceeded"), slog.Uint64("overflow_count", overflow)) - if r.output.Enabled(ctx, warning.Level) { - _ = r.emit(ctx, warning) - } - } - return r.emit(ctx, record) - } - window := max(r.settings.Window, policy.window) - hash := sha256.Sum256([]byte(key)) - e = &entry{key: key, fields: safeFields, level: record.Level, event: event, - fingerprint: hex.EncodeToString(hash[:16]), start: now, end: now.Add(window), window: window, - details: 1, sampleKey: policy.sample} - r.entries[key] = e - r.stats.Details++ - } else { - if e.idleRound { - e.details = 1 - e.idleRound = false - e.start, e.end = now, now.Add(e.window) - r.stats.Details++ - } else { - r.stats.Suppressed++ - } - } - e.count++ - if e.first.IsZero() { - e.first = now - } - e.last, e.idle = now, now - if sample, ok := fields[policy.sample]; ok && sample.Value.String() != "" && len(e.samples) < 3 { - value := sample.Value.String() - found := false - for _, previous := range e.samples { - if previous == value { - found = true - } - } - if !found { - e.samples = append(e.samples, value) - } - } - detail := e.count == 1 && e.details == 1 - r.mu.Unlock() - for _, p := range pending { - _ = r.emit(ctx, p) - } - if detail { - return r.emit(ctx, record) - } - return nil -} - -// settle is called under mu. Filtering still settles the window; discarded -// summaries are never replayed after a runtime level change. -func (r *Runtime) settle(e *entry, reason string) (slog.Record, bool) { - defer func() { e.count, e.details = 0, 0; e.first = time.Time{}; e.samples = nil }() - if e.count <= e.details || !r.output.Enabled(context.Background(), e.level) { - return slog.Record{}, false - } - ended := e.end - if reason != "window" && r.now().Before(ended) { - ended = r.now() - } - record := slog.NewRecord(r.now(), e.level, "重复故障汇总", 0) - record.AddAttrs(e.fields...) - record.AddAttrs(slog.String("event", "logging.error.summary"), slog.String("original_event", e.event), - slog.String("original_level", e.level.String()), slog.String("fingerprint", e.fingerprint), - slog.Time("window_started_at", e.start), slog.Time("window_ended_at", ended), - slog.Float64("window_seconds", e.window.Seconds()), slog.String("flush_reason", reason), - slog.Time("first_seen_at", e.first), slog.Time("last_seen_at", e.last), - slog.Uint64("occurrence_count", e.count), slog.Uint64("detail_count", e.details), - slog.Uint64("suppressed_count", e.count-e.details)) - if len(e.samples) > 0 { - // The only collection retained by deduplication is a bounded list of - // validated scalar IDs; no errors, records or contexts are retained. - record.AddAttrs(slog.Any("sample_"+e.sampleKey+"s", append([]string(nil), e.samples...))) - } - r.stats.Summaries++ - return record, true -} - -func (r *Runtime) drain(all bool, reason string) { - r.mu.Lock() - now := r.now() - var pending []slog.Record - for key, e := range r.entries { - if all || !now.Before(e.end) { - empty := e.count == 0 - if summary, ok := r.settle(e, reason); ok { - pending = append(pending, summary) - } - if all { - delete(r.entries, key) - continue - } - if now.Sub(e.idle) >= max(10*time.Minute, 2*e.window) { - delete(r.entries, key) - } else { - if empty || now.Sub(e.end) >= e.window { - e.idleRound = true - e.start, e.end = now, now.Add(e.window) - } else { - e.start = e.end - e.end = e.start.Add(e.window) - } - } - } - } - r.mu.Unlock() - for _, record := range pending { - _ = r.emit(context.Background(), record) - } -} - -func (r *Runtime) Flush() { r.drain(false, "window") } -func (r *Runtime) Close() error { - r.closeOnce.Do(func() { - close(r.stop) - <-r.done - r.mu.Lock() - r.closed = true - r.mu.Unlock() - r.drain(true, "shutdown") - }) - return nil -} - -// Recovered ends exactly the selected failure scope. The observing worker, -// rather than silence in the log, decides when a dependency has recovered. -func (r *Runtime) Recovered(ctx context.Context, event string, fields ...slog.Attr) { - scope := slog.NewRecord(r.now(), slog.LevelWarn, "", 0) - scope.AddAttrs(fields...) - scope.AddAttrs(slog.String("event", event)) - values := scalars(scope) - r.mu.Lock() - var pending []slog.Record - for key, e := range r.entries { - if e.event != event { - continue - } - match := true - for _, a := range e.fields { - if a.Key == "event" || a.Key == "error_code" || a.Key == "error_type" || a.Key == "sqlstate" { - continue - } - if value, ok := values[a.Key]; !ok || !value.Equal(a) { - match = false - break - } - } - if match { - if record, ok := r.settle(e, "recovered"); ok { - pending = append(pending, record) - } - delete(r.entries, key) - } - } - r.mu.Unlock() - for _, record := range pending { - _ = r.emit(ctx, record) - } -} diff --git a/monkeyai/backend/logging/dedup_test.go b/monkeyai/backend/logging/dedup_test.go deleted file mode 100644 index e9fd7dbe1..000000000 --- a/monkeyai/backend/logging/dedup_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package logging - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "log/slog" - "sync" - "testing" - "time" -) - -func runtimeTest(t *testing.T) (*Runtime, *bytes.Buffer, *slog.LevelVar, func(time.Duration)) { - t.Helper() - var output bytes.Buffer - level := new(slog.LevelVar) - level.Set(slog.LevelDebug) - r, err := NewRuntime(&output, level, Settings{true, time.Second, 128}) - if err != nil { - t.Fatal(err) - } - now := time.Unix(1700000000, 0) - r.mu.Lock() - r.now = func() time.Time { return now } - r.mu.Unlock() - t.Cleanup(func() { _ = r.Close() }) - return r, &output, level, func(d time.Duration) { r.mu.Lock(); now = now.Add(d); r.mu.Unlock() } -} -func records(t *testing.T, output *bytes.Buffer) []map[string]any { - t.Helper() - var result []map[string]any - decoder := json.NewDecoder(bytes.NewReader(output.Bytes())) - for { - var row map[string]any - err := decoder.Decode(&row) - if err == io.EOF { - return result - } - if err != nil { - t.Fatal(err) - } - result = append(result, row) - } -} -func repeatFailure(logger *slog.Logger, count int) { - for i := 0; i < count; i++ { - logger.Error("故障", "event", "model.call.completed", "operation", "proxy_request", "model_id", "m1", "request_id", fmt.Sprintf("r%d", i), "error_code", "connection_refused") - } -} -func TestDedupFixedWindowsAndIdle(t *testing.T) { - r, out, _, advance := runtimeTest(t) - derived := r.Logger().With("module", "proxy") - repeatFailure(derived, 1000) - advance(time.Second) - r.Flush() - rows := records(t, out) - if len(rows) != 2 || rows[1]["occurrence_count"] != float64(1000) || rows[1]["detail_count"] != float64(1) || rows[1]["suppressed_count"] != float64(999) { - t.Fatalf("首窗口计数错误: %v", rows) - } - if _, ok := rows[1]["request_id"]; ok { - t.Fatal("摘要含单一请求 ID") - } - repeatFailure(derived.WithGroup("gateway"), 600) - advance(time.Second) - r.Flush() - rows = records(t, out) - if len(rows) != 3 || rows[2]["occurrence_count"] != float64(600) || rows[2]["detail_count"] != float64(0) { - t.Fatalf("持续窗口计数错误: %v", rows) - } - advance(time.Second) - r.Flush() - repeatFailure(derived, 1) - if len(records(t, out)) != 4 { - t.Fatal("空闲后未直出首条") - } -} -func TestDedupFilteringCloseAndLateEvents(t *testing.T) { - r, out, level, advance := runtimeTest(t) - repeatFailure(r.Logger(), 10) - level.Set(slog.Level(12)) - advance(time.Second) - r.Flush() - level.Set(slog.LevelDebug) - r.Flush() - if len(records(t, out)) != 1 { - t.Fatal("重新调级补发了过期摘要") - } - repeatFailure(r.Logger(), 10) - _ = r.Close() - _ = r.Close() - rows := records(t, out) - if len(rows) != 2 || rows[1]["occurrence_count"] != float64(10) { - t.Fatalf("关闭刷新错误: %v", rows) - } - repeatFailure(r.Logger(), 2) - if len(records(t, out)) != 4 || r.Snapshot().ActiveKeys != 0 { - t.Fatal("迟到事件未直出") - } -} -func TestDedupCapacityAndUnknownEvents(t *testing.T) { - r, _, _, _ := runtimeTest(t) - for i := 0; i < 140; i++ { - r.Logger().Error("故障", "event", "model.call.completed", "operation", "proxy_request", "model_id", fmt.Sprint(i)) - } - if s := r.Snapshot(); s.ActiveKeys != 128 || s.Overflow != 12 { - t.Fatalf("容量错误: %+v", s) - } - r.Logger().Error("最终失败", "event", "billing.settlement.failed") - r.Logger().Error("最终失败", "event", "billing.settlement.failed") - if r.Snapshot().Suppressed != 0 { - t.Fatal("未登记事件被压制") - } -} -func TestDedupConcurrentAndRecovery(t *testing.T) { - r, out, _, _ := runtimeTest(t) - var wg sync.WaitGroup - for i := 0; i < 20; i++ { - wg.Go(func() { repeatFailure(r.Logger(), 50) }) - } - wg.Wait() - r.Recovered(context.Background(), "model.call.completed", slog.String("operation", "proxy_request"), slog.String("model_id", "m1")) - repeatFailure(r.Logger(), 1) - _ = r.Close() - rows := records(t, out) - if len(rows) != 3 || rows[1]["occurrence_count"] != float64(1000) { - t.Fatalf("恢复或并发计数错误: %v", rows) - } -} - -type failedOutput struct{} - -func (failedOutput) Write([]byte) (int, error) { return 0, io.ErrClosedPipe } -func TestOutputErrorStatistics(t *testing.T) { - r, err := NewRuntime(failedOutput{}, slog.LevelDebug, DefaultSettings()) - if err != nil { - t.Fatal(err) - } - defer r.Close() - r.Logger().Error("测试") - if r.Snapshot().OutputErrors != 1 { - t.Fatal("未统计输出故障") - } -} -func BenchmarkDedup(b *testing.B) { - for _, enabled := range []bool{false, true} { - b.Run(fmt.Sprint(enabled), func(b *testing.B) { - settings := DefaultSettings() - settings.Enabled = enabled - r, _ := NewRuntime(io.Discard, slog.LevelDebug, settings) - defer r.Close() - b.ReportAllocs() - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - repeatFailure(r.Logger(), 1) - } - }) - }) - } -} diff --git a/monkeyai/backend/logging/error.go b/monkeyai/backend/logging/error.go index b5a56a141..c27c35f84 100644 --- a/monkeyai/backend/logging/error.go +++ b/monkeyai/backend/logging/error.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "errors" + "fmt" "log/slog" "net" "net/url" @@ -21,7 +22,7 @@ type dependencyError struct { cause error } -func (e *dependencyError) Error() string { return "依赖检查失败" } +func (e *dependencyError) Error() string { return e.name + ": " + e.cause.Error() } func (e *dependencyError) Unwrap() error { return e.cause } func Dependency(name string, err error) error { if err == nil { @@ -35,7 +36,7 @@ type DiagnosticCoder interface { DiagnosticCode() string } -// ErrorAttrs never formats the error, URL, SQL, certificate or its payload. +// ErrorAttrs 保留原始错误文本及用于检索的分类字段。 func ErrorAttrs(err error) []slog.Attr { if err == nil { return nil @@ -106,7 +107,7 @@ func ErrorAttrs(err error) []slog.Attr { if inner, ok := innerURLCause(err); ok { extra = append(extra, slog.String("cause_type", reflect.TypeOf(inner).String())) } - attrs := []slog.Attr{slog.String("error_code", code), slog.String("error_type", reflect.TypeOf(cause).String())} + attrs := []slog.Attr{slog.String("error_code", code), slog.String("error_type", reflect.TypeOf(cause).String()), slog.String("error_message", fmt.Sprint(err))} var dependency *dependencyError if errors.As(err, &dependency) { attrs = append(attrs, slog.String("dependency", dependency.name)) @@ -131,8 +132,7 @@ func tlsReason(err error) string { return "unknown" } -// innerURLCause unwraps nested *url.Error values so logs can show the root -// transport error's type without exposing its URL or message. +// innerURLCause 提取 URL 错误链中最内层的传输错误。 func innerURLCause(err error) (error, bool) { cause := err for range 8 { diff --git a/monkeyai/backend/logging/error_test.go b/monkeyai/backend/logging/error_test.go index 5f858361f..e95b281f0 100644 --- a/monkeyai/backend/logging/error_test.go +++ b/monkeyai/backend/logging/error_test.go @@ -16,7 +16,7 @@ import ( "github.com/jackc/pgx/v5/pgconn" ) -func TestErrorClassificationAndWrappedSecrets(t *testing.T) { +func TestErrorClassificationAndWrappedMessages(t *testing.T) { for _, test := range []struct { name string err error @@ -38,10 +38,13 @@ func TestErrorClassificationAndWrappedSecrets(t *testing.T) { values := map[string]string{} for _, a := range attrs { values[a.Key] = a.Value.String() - if strings.Contains(a.Value.String(), "private-") { + if a.Key != "error_message" && strings.Contains(a.Value.String(), "private-") { t.Fatal("错误分类泄漏秘密", a) } } + if values["error_message"] != fmt.Sprint(Dependency("database", err)) { + t.Fatalf("原始错误丢失: %v", values) + } if values["error_code"] != test.code || values["dependency"] != "database" || test.kind != "" && values["error_type"] != test.kind { t.Fatalf("分类错误: %v", values) } @@ -78,7 +81,7 @@ func TestErrorAttrsKeepsURLRootCause(t *testing.T) { values := map[string]string{} for _, a := range ErrorAttrs(err) { values[a.Key] = a.Value.String() - if strings.Contains(a.Value.String(), "private-") || strings.Contains(a.Value.String(), "example.test") { + if a.Key != "error_message" && (strings.Contains(a.Value.String(), "private-") || strings.Contains(a.Value.String(), "example.test")) { t.Fatal("根错误分类泄漏秘密", a) } } @@ -93,9 +96,9 @@ func (e domainError) Error() string { return "domain failure" } func (e domainError) DiagnosticCode() string { return e.code } func TestEventKeepsPollDiagnostics(t *testing.T) { - r, out, _, _ := runtimeTest(t) + logger, out := testLogger() err := &url.Error{Op: "Get", URL: "https://private-host.example/path?video_id=private", Err: domainError{code: "upstream_address_blocked"}} - Event(t.Context(), r.Logger(), slog.LevelWarn, "video.job.retry", "重试", "module", "videogen", "operation", "poll", "job_id", "job-1", "failures", int32(1), "phase", "query_provider", "proxy_used", true, "error", err) + Event(t.Context(), logger, slog.LevelWarn, "video.job.retry", "重试", "module", "videogen", "operation", "poll", "job_id", "job-1", "failures", int32(1), "phase", "query_provider", "proxy_used", true, "error", err) row := records(t, out)[0] if row["phase"] != "query_provider" || row["proxy_used"] != true { t.Fatalf("阶段或代理标记丢失: %v", row) @@ -104,14 +107,14 @@ func TestEventKeepsPollDiagnostics(t *testing.T) { if !ok || fields["error_code"] != "upstream_address_blocked" || fields["error_type"] != "*url.Error" || fields["cause_type"] != "logging.domainError" { t.Fatalf("根错误字段错误: %v", row) } - if strings.Contains(out.String(), "private") { + if fields["error_message"] != err.Error() { t.Fatal(out.String()) } } func TestDerivedHandlerPreservesSource(t *testing.T) { - r, out, _, _ := runtimeTest(t) - Event(t.Context(), r.Logger().With("module", "proxy").WithGroup("call"), slog.LevelInfo, "test.event", "测试") + logger, out := testLogger() + Event(t.Context(), logger.With("module", "proxy").WithGroup("call"), slog.LevelInfo, "test.event", "测试") row := records(t, out)[0] source, ok := row["source"].(map[string]any) if !ok || source["file"] != "error_test.go" || !strings.Contains(source["function"].(string), "TestDerivedHandlerPreservesSource") { diff --git a/monkeyai/backend/logging/http.go b/monkeyai/backend/logging/http.go index bbfbb6c82..199939021 100644 --- a/monkeyai/backend/logging/http.go +++ b/monkeyai/backend/logging/http.go @@ -44,11 +44,13 @@ func HTTP(logger *slog.Logger) func(http.Handler) http.Handler { } } Diagnostic(ctx, logger, slog.LevelError, "http.request.panicked", nil, - slog.String("error_type", fmt.Sprintf("%T", fault)), slog.Attr{Key: "stack", Value: slog.GroupValue(stack...)}) + slog.String("error_type", fmt.Sprintf("%T", fault)), slog.String("error_message", fmt.Sprint(fault)), slog.Attr{Key: "stack", Value: slog.GroupValue(stack...)}) if writer.status == 0 && !writer.hijacked { writer.Header().Set("Content-Type", "application/json; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) - _, _ = io.WriteString(writer, "{\"error\":{\"code\":\"internal_error\",\"message\":\"服务内部错误\"}}\n") + if _, err := io.WriteString(writer, "{\"error\":{\"code\":\"internal_error\",\"message\":\"服务内部错误\"}}\n"); err != nil { + Diagnostic(ctx, logger, slog.LevelError, "http.response.failed", err) + } } else { abort = true } @@ -64,14 +66,6 @@ func HTTP(logger *slog.Logger) func(http.Handler) http.Handler { diagnosed, expected := state.diagnosed, state.expected state.mu.Unlock() failed := writer.status >= 500 || writer.aborted || r.Context().Err() != nil - if rt := runtimeFor(logger); rt != nil { - rt.mu.Lock() - rt.stats.Requests++ - if failed { - rt.stats.FailedRequests++ - } - rt.mu.Unlock() - } result := "completed" if writer.hijacked { result = "upgraded" diff --git a/monkeyai/backend/logging/http_test.go b/monkeyai/backend/logging/http_test.go index eac317dae..35f69e373 100644 --- a/monkeyai/backend/logging/http_test.go +++ b/monkeyai/backend/logging/http_test.go @@ -17,12 +17,11 @@ import ( func TestHTTPRequestIdentityAndDiagnosticOwnership(t *testing.T) { var out bytes.Buffer - runtime, _ := NewRuntime(&out, slog.LevelDebug, DefaultSettings()) - defer runtime.Close() + logger := New(&out, slog.LevelDebug) router := chi.NewRouter() - router.Use(HTTP(runtime.Logger())) + router.Use(HTTP(logger)) router.Get("/items/{id}", func(w http.ResponseWriter, r *http.Request) { - Diagnostic(r.Context(), runtime.Logger(), slog.LevelError, "resource.operation.failed", errors.New("private")) + Diagnostic(r.Context(), logger, slog.LevelError, "resource.operation.failed", errors.New("数据库连接失败")) w.Header().Set("X-Request-ID", "upstream-forgery") w.WriteHeader(503) }) @@ -43,8 +42,8 @@ func TestHTTPRequestIdentityAndDiagnosticOwnership(t *testing.T) { t.Fatal("日志泄漏", secret) } } - if s := runtime.Snapshot(); s.Requests != 1 || s.FailedRequests != 1 { - t.Fatalf("请求计数错误: %+v", s) + if rows[0]["error_message"] != "数据库连接失败" { + t.Fatalf("原始错误丢失: %v", rows) } } func TestHTTPPanicAndAbort(t *testing.T) { @@ -88,8 +87,11 @@ func TestHTTPPanicAndAbort(t *testing.T) { t.Fatalf("异常恢复未返回通用 JSON 错误: %s", response.Body.String()) } } - if strings.Contains(out.String(), "secret-panic-value") || strings.Contains(response.Body.String(), "secret-panic-value") { - t.Fatal("panic 泄漏") + if strings.Contains(response.Body.String(), "secret-panic-value") { + t.Fatal("panic 泄漏到响应") + } + if !test.control && records(t, &out)[0]["error_message"] != "secret-panic-value" { + t.Fatal("panic 原因丢失") } }) } diff --git a/monkeyai/backend/logging/lifecycle_test.go b/monkeyai/backend/logging/lifecycle_test.go deleted file mode 100644 index 4d4d8de40..000000000 --- a/monkeyai/backend/logging/lifecycle_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package logging - -import ( - "io" - "log/slog" - "runtime" - "sync" - "testing" -) - -func TestConcurrentCloseAccountsForEveryEvent(t *testing.T) { - r, out, _, _ := runtimeTest(t) - var wg sync.WaitGroup - for i := 0; i < 8; i++ { - wg.Go(func() { repeatFailure(r.Logger(), 100) }) - } - wg.Go(func() { _ = r.Close() }) - wg.Go(func() { _ = r.Close() }) - wg.Wait() - _ = r.Close() - var occurrences float64 - for _, row := range records(t, out) { - if row["event"] == "logging.error.summary" { - occurrences += row["suppressed_count"].(float64) - } else { - occurrences++ - } - } - stats := r.Snapshot() - if occurrences != 800 || stats.Candidates != 800 || stats.Details+stats.Suppressed != 800 || stats.ActiveKeys != 0 { - t.Fatalf("并发关闭漏计: %.0f %+v", occurrences, stats) - } -} -func TestRuntimeChurnKeepsSchedulersAndStateBounded(t *testing.T) { - before := runtime.NumGoroutine() - for pass := 0; pass < 8; pass++ { - r, err := NewRuntime(io.Discard, slog.LevelDebug, Settings{true, DefaultSettings().Window, 128}) - if err != nil { - t.Fatal(err) - } - for i := 0; i < 1000; i++ { - r.Logger().Error("故障", "event", "model.call.completed", "operation", "proxy_request", "model_id", stringID(i)) - } - if r.Snapshot().ActiveKeys != 128 { - t.Fatal("指纹容量失控") - } - if runtime.NumGoroutine() > before+3 { - t.Fatal("为每个指纹创建了调度器") - } - _ = r.Close() - if r.Snapshot().ActiveKeys != 0 { - t.Fatal("关闭后仍保留聚合状态") - } - } - if runtime.NumGoroutine() > before+2 { - t.Fatal("日志调度器未退出") - } -} diff --git a/monkeyai/backend/logging/logger.go b/monkeyai/backend/logging/logger.go index fd77d5e40..db1260e54 100644 --- a/monkeyai/backend/logging/logger.go +++ b/monkeyai/backend/logging/logger.go @@ -3,6 +3,7 @@ package logging import ( "context" + "fmt" "io" "log" "log/slog" @@ -32,10 +33,9 @@ func New(output io.Writer, level slog.Leveler) *slog.Logger { } type safeHandler struct { - next slog.Handler - runtime *Runtime - attrs []slog.Attr - groups []string + next slog.Handler + attrs []slog.Attr + groups []string } func (h *safeHandler) Enabled(ctx context.Context, level slog.Level) bool { @@ -90,10 +90,9 @@ func (h *safeHandler) Handle(ctx context.Context, record slog.Record) error { attrs = append(attrs, contextAttrs(ctx)...) clean := slog.NewRecord(record.Time, record.Level, record.Message, record.PC) clean.AddAttrs(mergeAttrs(attrs)...) - if h.runtime != nil { - return h.runtime.submit(ctx, clean) - } - return h.next.Handle(ctx, clean) + err := h.next.Handle(ctx, clean) + reportOutput(err) + return err } func (h *safeHandler) WithAttrs(attrs []slog.Attr) slog.Handler { @@ -130,13 +129,22 @@ func ErrorLog(logger *slog.Logger) *log.Logger { return log.New(runtimeWriter{lo type runtimeWriter struct{ logger *slog.Logger } func (w runtimeWriter) Write(p []byte) (int, error) { - w.logger.Error("HTTP 运行时错误", "event", "runtime.http.error") + w.logger.Error("HTTP 运行时错误", "event", "runtime.http.error", "error_message", string(p)) return len(p), nil } -func runtimeFor(logger *slog.Logger) *Runtime { - if h, ok := logger.Handler().(*safeHandler); ok { - return h.runtime +func reportOutput(err error) { + if err == nil { + return + } + if _, fallback := fmt.Fprintln(os.Stderr, "结构化日志输出失败:", err); fallback != nil { + println("结构化日志及 stderr 输出失败:", err.Error(), fallback.Error()) + } +} + +func handle(ctx context.Context, logger *slog.Logger, record slog.Record) { + err := logger.Handler().Handle(ctx, record) + if _, ok := logger.Handler().(*safeHandler); !ok { + reportOutput(err) } - return nil } diff --git a/monkeyai/backend/logging/logger_test.go b/monkeyai/backend/logging/logger_test.go index 42f5d145e..e34c4ff8a 100644 --- a/monkeyai/backend/logging/logger_test.go +++ b/monkeyai/backend/logging/logger_test.go @@ -5,10 +5,13 @@ import ( "context" "encoding/json" "errors" + "fmt" + "io" "log/slog" "net/url" "os" "strings" + "sync" "syscall" "testing" ) @@ -58,16 +61,16 @@ func TestSafetyBeforeJSONResolution(t *testing.T) { t.Fatal(err) } logger.Error("wrapped", "error", &url.Error{Op: "get", URL: "https://private.test?token=secret", Err: errors.New("private response")}) - if strings.Contains(output.String(), "private.test") || strings.Contains(output.String(), "private response") { + if records(t, &output)[1]["error"].(map[string]any)["error_message"] != `get "https://private.test?token=secret": private response` { t.Fatal(output.String()) } } -func TestSafeRuntimeAdapter(t *testing.T) { +func TestRuntimeAdapterPreservesError(t *testing.T) { var output bytes.Buffer logger := New(&output, slog.LevelInfo) ErrorLog(logger).Print("panic secret https://private.test?token=hidden") - if strings.Contains(output.String(), "secret") || !strings.Contains(output.String(), "runtime.http.error") { + if records(t, &output)[0]["error_message"] != "panic secret https://private.test?token=hidden\n" || !strings.Contains(output.String(), "runtime.http.error") { t.Fatal(output.String()) } attrs := ErrorAttrs(context.DeadlineExceeded) @@ -76,7 +79,7 @@ func TestSafeRuntimeAdapter(t *testing.T) { } } -func TestErrorAttrsDoNotFormatErrors(t *testing.T) { +func TestErrorAttrsPreservesMessages(t *testing.T) { _ = ErrorAttrs(hostile{}) for _, test := range []struct { cause error @@ -91,8 +94,114 @@ func TestErrorAttrsDoNotFormatErrors(t *testing.T) { logger := New(&output, slog.LevelInfo) err := &os.PathError{Op: "private-operation", Path: "private-file", Err: test.cause} logger.Error("safe", "error", err) - if !strings.Contains(output.String(), test.code) || strings.Contains(output.String(), "private-") { + if !strings.Contains(output.String(), test.code) || records(t, &output)[0]["error"].(map[string]any)["error_message"] != err.Error() { t.Fatal(output.String()) } } } + +func testLogger() (*slog.Logger, *bytes.Buffer) { + output := new(bytes.Buffer) + return New(output, slog.LevelDebug), output +} + +func records(t *testing.T, output *bytes.Buffer) []map[string]any { + t.Helper() + var result []map[string]any + decoder := json.NewDecoder(bytes.NewReader(output.Bytes())) + for { + var row map[string]any + err := decoder.Decode(&row) + if err == io.EOF { + return result + } + if err != nil { + t.Fatal(err) + } + result = append(result, row) + } +} + +func TestRepeatedErrorsPreserveEveryOccurrence(t *testing.T) { + logger, output := testLogger() + logger = logger.With("module", "proxy").WithGroup("call") + var wg sync.WaitGroup + for i := range 100 { + wg.Go(func() { + err := fmt.Errorf("请求 %d: %w", i, io.ErrUnexpectedEOF) + Event(t.Context(), logger, slog.LevelError, "model.call.completed", "模型调用失败", + "model_id", "m1", "operation", "proxy_request", "request_id", fmt.Sprintf("r%d", i), "error", err) + }) + } + wg.Wait() + rows := records(t, output) + if len(rows) != 100 { + t.Fatalf("错误被合并: %d", len(rows)) + } + seen := make(map[string]bool) + for _, row := range rows { + call := row["call"].(map[string]any) + id := call["request_id"].(string) + if seen[id] { + t.Fatalf("重复请求: %s", id) + } + seen[id] = true + expected := fmt.Sprintf("请求 %s: unexpected EOF", strings.TrimPrefix(id, "r")) + if call["event"] != "model.call.completed" || call["error"].(map[string]any)["error_message"] != expected { + t.Fatalf("错误详情丢失: %v", row) + } + } +} + +func TestErrorMessagesSurviveAllEntryPoints(t *testing.T) { + message := "数据库查询失败:" + strings.Repeat("column does not exist; ", 20) + "\n原始错误" + for _, test := range []struct { + name string + log func(*slog.Logger) + }{ + {"诊断", func(logger *slog.Logger) { + Diagnostic(t.Context(), logger, slog.LevelError, "resource.operation.failed", errors.New(message)) + }}, + {"错误属性", func(logger *slog.Logger) { + logger.LogAttrs(t.Context(), slog.LevelError, "失败", ErrorAttrs(errors.New(message))...) + }}, + {"文本属性", func(logger *slog.Logger) { logger.Error("失败", "error_message", message) }}, + {"派生属性", func(logger *slog.Logger) { logger.With("error_message", message).Error("失败") }}, + } { + t.Run(test.name, func(t *testing.T) { + logger, output := testLogger() + test.log(logger) + if records(t, output)[0]["error_message"] != message { + t.Fatal("错误文本被过滤或截断") + } + }) + } +} + +type brokenOutput struct{} + +func (brokenOutput) Write([]byte) (int, error) { return 0, io.ErrClosedPipe } + +func TestOutputFailureFallsBackToStderr(t *testing.T) { + file, err := os.CreateTemp(t.TempDir(), "stderr") + if err != nil { + t.Fatal(err) + } + original := os.Stderr + os.Stderr = file + t.Cleanup(func() { + os.Stderr = original + if err := file.Close(); err != nil { + t.Error(err) + } + }) + New(brokenOutput{}, slog.LevelDebug).Error("业务失败", "error", errors.New("数据库不可用")) + Event(t.Context(), slog.New(slog.NewJSONHandler(brokenOutput{}, nil)), slog.LevelError, "test.failed", "业务失败") + content, err := os.ReadFile(file.Name()) + if err != nil { + t.Fatal(err) + } + if strings.Count(string(content), io.ErrClosedPipe.Error()) != 2 { + t.Fatalf("日志输出错误被忽略: %s", content) + } +} diff --git a/monkeyai/backend/logging/policy.go b/monkeyai/backend/logging/policy.go deleted file mode 100644 index d30995af4..000000000 --- a/monkeyai/backend/logging/policy.go +++ /dev/null @@ -1,77 +0,0 @@ -package logging - -import ( - "encoding/json" - "log/slog" - "time" -) - -type policy struct { - scope []string - sample string - window time.Duration -} - -var policies = map[string]policy{ - "model.call.completed": {scope: []string{"model_id", "operation"}, sample: "request_id"}, - "model.usage.failed": {scope: []string{"model_id"}, sample: "request_id"}, - "http.request.failed": {scope: []string{"route", "method"}, sample: "request_id"}, - "resource.operation.failed": {scope: []string{"route", "method"}, sample: "request_id"}, - "readiness.failed": {scope: []string{"dependency"}, sample: "request_id"}, - "relay.stream.failed": {scope: []string{"server_id", "user_id", "machine_id", "operation", "phase"}, sample: "connection_id"}, - "resource.upload.operation.failed": {scope: []string{"upload_id", "phase"}, sample: "request_id"}, - "mcp.credential.retry": {scope: []string{"connector_id", "operation"}, sample: "worker_run_id", window: 5 * time.Minute}, - "resource.upload.failed": {scope: []string{"upload_id", "chunk_index", "phase"}, sample: "request_id"}, - "billing.retry.failed": {scope: []string{"transaction_id", "operation"}, sample: "transaction_id", window: 5 * time.Minute}, - "image.job.retry": {scope: []string{"job_id", "operation"}, sample: "job_id", window: 5 * time.Minute}, - "video.job.retry": {scope: []string{"job_id", "operation"}, sample: "job_id", window: 5 * time.Minute}, - "worker.scan.failed": {scope: []string{"module", "operation", "dependency"}, sample: "worker_run_id", window: 5 * time.Minute}, -} - -func scalars(record slog.Record) map[string]slog.Attr { - result := make(map[string]slog.Attr) - var add func(slog.Attr) - add = func(a slog.Attr) { - if a.Value.Kind() == slog.KindGroup { - for _, child := range a.Value.Group() { - add(child) - } - return - } - if a.Value.Kind() != slog.KindAny && a.Value.Kind() != slog.KindLogValuer { - result[a.Key] = a - } - } - record.Attrs(func(a slog.Attr) bool { add(a); return true }) - return result -} - -func fingerprint(level slog.Level, fields map[string]slog.Attr, p policy) (string, []slog.Attr) { - if level < slog.LevelWarn { - return "", nil - } - keys := []string{"event", "module", "operation", "error_code", "error_type", "upstream_status", "sqlstate"} - for _, key := range p.scope { - if a, ok := fields[key]; !ok || a.Value.String() == "" { - return "", nil - } - } - keys = append(keys, p.scope...) - seen := make(map[string]bool) - tuple := []any{level.String()} - var attrs []slog.Attr - for _, key := range keys { - if seen[key] { - continue - } - seen[key] = true - if a, ok := fields[key]; ok { - tuple = append(tuple, key, a.Value.String()) - if key != "event" { - attrs = append(attrs, a) - } - } - } - data, _ := json.Marshal(tuple) - return string(data), attrs -} diff --git a/monkeyai/backend/logging/safe.go b/monkeyai/backend/logging/safe.go index 473581c19..16c279429 100644 --- a/monkeyai/backend/logging/safe.go +++ b/monkeyai/backend/logging/safe.go @@ -17,7 +17,6 @@ var stringFields = map[string]bool{ "previous_level": true, "current_level": true, "origin": true, "intent": true, "code": true, "kind": true, "status": true, "provider": true, "usage_error_code": true, "file": true, "resource_id": true, "key_id": true, - "original_event": true, "original_level": true, "fingerprint": true, "flush_reason": true, "input_source": true, "output_source": true, } @@ -29,7 +28,7 @@ func sensitive(key string) bool { } } switch key { - case "key", "path", "url", "endpoint", "body", "query", "args", "config", "dsn", "message", "error_message", "panic", "source", "time", "level", "msg": + case "key", "path", "url", "endpoint", "body", "query", "args", "config", "dsn", "message", "panic", "source", "time", "level", "msg": return true } return false @@ -40,10 +39,7 @@ func safeAttr(a slog.Attr) slog.Attr { return slog.Attr{} } switch a.Value.Kind() { - case slog.KindLogValuer: - // JSONHandler resolves LogValuer before ReplaceAttr, so filter here. - return slog.Attr{} - case slog.KindAny: + case slog.KindAny, slog.KindLogValuer: if err, ok := a.Value.Any().(error); ok && err != nil { attrs := ErrorAttrs(err) return slog.Attr{Key: a.Key, Value: slog.GroupValue(attrs...)} @@ -65,6 +61,9 @@ func safeAttr(a slog.Attr) slog.Attr { if value == "" { return slog.Attr{} } + if a.Key == "error_message" || a.Key == "error" || a.Key == "err" { + return a + } if a.Key == "function" { if len(value) > 256 { return slog.Attr{} diff --git a/monkeyai/backend/logging/worker.go b/monkeyai/backend/logging/worker.go index d2cb4c2b4..f8834ae62 100644 --- a/monkeyai/backend/logging/worker.go +++ b/monkeyai/backend/logging/worker.go @@ -49,9 +49,6 @@ func (w *Worker) Report(ctx context.Context, err error) { w.logger.LogAttrs(ctx, slog.LevelWarn, "后台依赖检查失败", fields...) return } - if r := runtimeFor(w.logger); r != nil { - r.Recovered(ctx, "worker.scan.failed", fields...) - } fields = append(fields, slog.String("event", "dependency.recovered"), slog.Uint64("failure_count", count), slog.Int64("duration_ms", time.Since(first).Milliseconds())) w.logger.LogAttrs(ctx, slog.LevelInfo, "后台依赖已恢复", fields...) } diff --git a/monkeyai/backend/logging/worker_test.go b/monkeyai/backend/logging/worker_test.go index fd8c0bf33..d6334b714 100644 --- a/monkeyai/backend/logging/worker_test.go +++ b/monkeyai/backend/logging/worker_test.go @@ -4,57 +4,31 @@ import ( "context" "io" "log/slog" - "sync/atomic" "testing" - "time" ) -func TestWorkerRecoveryFlushesBeforeRecovery(t *testing.T) { - r, out, _, _ := runtimeTest(t) - worker := NewWorker(r.Logger(), "resourceimport", "cleanup_uploads", "object_storage") +func TestWorkerReportsEveryFailureAndRecovery(t *testing.T) { + logger, out := testLogger() + worker := NewWorker(logger, "resourceimport", "cleanup_uploads", "object_storage") worker.Report(context.Background(), io.ErrUnexpectedEOF) worker.Report(context.Background(), io.ErrUnexpectedEOF) worker.Report(context.Background(), nil) worker.Report(context.Background(), nil) worker.Report(context.Background(), io.ErrUnexpectedEOF) rows := records(t, out) - if len(rows) != 4 || rows[1]["event"] != "logging.error.summary" || rows[1]["occurrence_count"] != float64(2) || rows[2]["event"] != "dependency.recovered" || rows[2]["failure_count"] != float64(2) { + if len(rows) != 4 || rows[1]["event"] != "worker.scan.failed" || rows[1]["error_message"] != io.ErrUnexpectedEOF.Error() || rows[2]["event"] != "dependency.recovered" || rows[2]["failure_count"] != float64(2) { t.Fatalf("恢复顺序或计数错误: %v", rows) } } -type countWriter struct{ count atomic.Uint64 } - -func (w *countWriter) Write(p []byte) (int, error) { w.count.Add(1); return len(p), nil } -func TestSchedulerFlushesWithoutNewEvents(t *testing.T) { - var out countWriter - r, err := NewRuntime(&out, slog.LevelError, Settings{true, time.Second, 128}) - if err != nil { - t.Fatal(err) - } - defer r.Close() - repeatFailure(r.Logger(), 2) - deadline := time.NewTimer(3 * time.Second) - defer deadline.Stop() - ticker := time.NewTicker(10 * time.Millisecond) - defer ticker.Stop() - for out.count.Load() < 2 { - select { - case <-deadline.C: - t.Fatal("无后续请求时未刷新摘要") - case <-ticker.C: - } - } -} func BenchmarkLoggerDistribution(b *testing.B) { for _, count := range []int{1, 4096, 5000} { b.Run(stringID(count), func(b *testing.B) { - r, _ := NewRuntime(io.Discard, slog.LevelDebug, DefaultSettings()) - defer r.Close() + logger := New(io.Discard, slog.LevelDebug) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - r.Logger().Error("故障", "event", "model.call.completed", "model_id", stringID(i%count), "operation", "proxy_request") + logger.Error("故障", "event", "model.call.completed", "model_id", stringID(i%count), "operation", "proxy_request") } }) } diff --git a/monkeyai/backend/server/reset.go b/monkeyai/backend/server/reset.go index 35a4290e4..fed0f1108 100644 --- a/monkeyai/backend/server/reset.go +++ b/monkeyai/backend/server/reset.go @@ -7,6 +7,7 @@ import ( "flag" "fmt" "io" + "log/slog" "net/mail" "os" "strings" @@ -37,6 +38,7 @@ func Reset(ctx context.Context, args []string, input io.Reader, output io.Writer } pool, err := database.Open(ctx, url) if err != nil { + slog.WarnContext(ctx, "操作失败", "module", "server", "operation", "Reset", "error", err) return errors.New("无法连接数据库,请检查 MONKEYAI_DATABASE_URL 或 --database-url") } defer pool.Close() @@ -75,5 +77,7 @@ func Reset(ctx context.Context, args []string, input io.Reader, output io.Writer } func PrintResetUsage(output io.Writer) { - _, _ = io.WriteString(output, "用法: monkeyai-server reset [--database-url PostgreSQL连接地址]\n优先重置 admin@baizhi.cloud;未找到时提示填写管理员邮箱。\n数据库连接默认读取 MONKEYAI_DATABASE_URL,成功后输出账号和随机新密码。\n") + if _, err := io.WriteString(output, "用法: monkeyai-server reset [--database-url PostgreSQL连接地址]\n优先重置 admin@baizhi.cloud;未找到时提示填写管理员邮箱。\n数据库连接默认读取 MONKEYAI_DATABASE_URL,成功后输出账号和随机新密码。\n"); err != nil { + slog.Error("写入输出失败", "module", "server", "operation", "PrintResetUsage", "error", err) + } } diff --git a/monkeyai/backend/server/reset_test.go b/monkeyai/backend/server/reset_test.go index 9b1936545..2eb8ca21b 100644 --- a/monkeyai/backend/server/reset_test.go +++ b/monkeyai/backend/server/reset_test.go @@ -171,7 +171,7 @@ func TestResetFlags(t *testing.T) { func TestEmbeddedResetCommand(t *testing.T) { t.Setenv("MONKEYAI_DATABASE_URL", "") args := []string{"reset", "--database-url", "postgres://test", "--help"} - if _, _, err := ParseLogging([]string{"reset", "--database-url", "postgres://test"}); err != nil { + if _, err := ParseLogLevel([]string{"reset", "--database-url", "postgres://test"}); err != nil { t.Fatalf("嵌入入口未识别 reset: %v", err) } if _, err := ParseLogLevel([]string{"reset"}); err != nil { diff --git a/monkeyai/backend/server/server.go b/monkeyai/backend/server/server.go index 89495f0f4..370180d3a 100644 --- a/monkeyai/backend/server/server.go +++ b/monkeyai/backend/server/server.go @@ -9,7 +9,6 @@ import ( "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/app" "github.com/chaitin/MonkeyCode/monkeyai/backend/internal/config" - "github.com/chaitin/MonkeyCode/monkeyai/backend/logging" "github.com/chaitin/MonkeyCode/monkeyai/backend/member" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5/pgxpool" @@ -32,13 +31,6 @@ func ParseLogLevel(args []string) (slog.Level, error) { return config.ParseLogLevel(args) } -func ParseLogging(args []string) (slog.Level, logging.Settings, error) { - if len(args) > 0 && args[0] == "reset" { - args = args[1:] - } - return config.ParseLogging(args) -} - // PrintUsage writes startup help without reading environment secrets. func PrintUsage(output io.Writer) { config.PrintUsage(output) } diff --git a/monkeyai/design/error-logging-optimization-plan.md b/monkeyai/design/error-logging-optimization-plan.md index efc985913..db03437d6 100644 --- a/monkeyai/design/error-logging-optimization-plan.md +++ b/monkeyai/design/error-logging-optimization-plan.md @@ -1,5 +1,7 @@ # 后端错误日志优化方案 +> 后续修订:已移除日志去重、定时摘要及 Runtime,入口使用 `logging.New`,错误原文写入 `error_message`。下文保留原方案及历史验收记录,当前行为以[运行日志说明](../backend/docs/logging.md)为准。 + - 创建日期:2026-10-16;修订日期:2026-10-21 - 代码基线:最初基于 `5b5c3d91` 实施;提交 PR 前同步 `a1cccb86` 主线,保留统计小时汇总和会话分组修复。 - 状态:后端日志和容器日志轮转已完成;已执行专用数据库、对象存储、协议、预览浏览器和竞态回归。2026-10-21 调整范围,暂缓 Nginx 日志改造并还原相关配置。性能及验证边界见[实施验收记录](../backend/docs/logging-validation.md),不代表镜像已发布或部署。 diff --git a/monkeyai/docker-compose.yml b/monkeyai/docker-compose.yml index 4bd250176..91b1b083e 100644 --- a/monkeyai/docker-compose.yml +++ b/monkeyai/docker-compose.yml @@ -83,9 +83,6 @@ services: MONKEYAI_HTTP_ADDR: :8080 TMPDIR: /app/import-tmp MONKEYAI_LOG_LEVEL: ${MONKEYAI_LOG_LEVEL:-warn} - MONKEYAI_LOG_DEDUP_ENABLED: ${MONKEYAI_LOG_DEDUP_ENABLED:-true} - MONKEYAI_LOG_DEDUP_WINDOW: ${MONKEYAI_LOG_DEDUP_WINDOW:-60s} - MONKEYAI_LOG_DEDUP_MAX_KEYS: ${MONKEYAI_LOG_DEDUP_MAX_KEYS:-4096} MONKEYAI_ENDPOINT_MAX_CONNECTIONS: ${MONKEYAI_ENDPOINT_MAX_CONNECTIONS:-1000} MONKEYAI_RUNTIME_RELAY: ${MONKEYAI_RUNTIME_RELAY:-true} MONKEYAI_RUNTIME_STATE_DIR: /app/runtime