Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions monkeyai/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 5 additions & 9 deletions monkeyai/backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions monkeyai/backend/docs/logging-validation.md
Original file line number Diff line number Diff line change
@@ -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 注入与共享动态阈值。
Expand Down
19 changes: 9 additions & 10 deletions monkeyai/backend/docs/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 的输出格式、开关和日志诊断范围。
4 changes: 3 additions & 1 deletion monkeyai/backend/internal/apikey/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion monkeyai/backend/internal/apikey/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
15 changes: 13 additions & 2 deletions monkeyai/backend/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
})
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions monkeyai/backend/internal/app/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"log/slog"
"math"
"net/http"

Expand Down Expand Up @@ -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"} {
Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions monkeyai/backend/internal/app/design.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"context"
"log/slog"
"uuid"

"github.com/chaitin/MonkeyCode/monkeyai/backend/internal/designcatalog"
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions monkeyai/backend/internal/audit/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 转发;公网或本机直连不信任代理头。
Expand All @@ -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()
Expand All @@ -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()
Expand Down
13 changes: 12 additions & 1 deletion monkeyai/backend/internal/audit/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import (
"context"
"encoding/json"
"errors"
"github.com/chaitin/MonkeyCode/monkeyai/backend/logging"
"log/slog"
"net/http"
"slices"
"strconv"
"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"
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
}
Expand All @@ -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
Expand Down
26 changes: 25 additions & 1 deletion monkeyai/backend/internal/billing/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"slices"
"strconv"
Expand All @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
Loading
Loading