diff --git a/cmd/bots/botcheck/main.go b/cmd/bots/botcheck/main.go index 4de8b6c8..705fbe73 100644 --- a/cmd/bots/botcheck/main.go +++ b/cmd/bots/botcheck/main.go @@ -106,16 +106,16 @@ func main() { priv, err := mtprotoedge.LoadOrGenerateRSAKey(*rsaPath) if err != nil { - logger.Fatal("加载 RSA key 失败", zap.Error(err)) + logger.Fatal("load RSA key failed", zap.Error(err)) } host, portStr, err := net.SplitHostPort(*addr) if err != nil { - logger.Fatal("解析地址失败", zap.Error(err)) + logger.Fatal("parse address failed", zap.Error(err)) } port, err := strconv.Atoi(portStr) if err != nil { - logger.Fatal("解析端口失败", zap.Error(err)) + logger.Fatal("parse port failed", zap.Error(err)) } var client *telegram.Client @@ -240,7 +240,7 @@ func main() { <-ctx.Done() return nil }); err != nil { - logger.Fatal("运行失败", zap.Error(err)) + logger.Fatal("run failed", zap.Error(err)) } fmt.Println("已退出") } @@ -286,7 +286,7 @@ func echoUpdates(ctx context.Context, raw *tg.Client, u tg.UpdatesClass, logger Message: "echo: " + msg.Message, RandomID: rid, }); err != nil { - logger.Warn("echo 回复失败", zap.Int64("to", from), zap.Error(err)) + logger.Warn("echo reply failed", zap.Int64("to", from), zap.Error(err)) continue } fmt.Printf(">> 已回复 %d: %q\n", from, "echo: "+msg.Message) diff --git a/cmd/bots/botdemo/main.go b/cmd/bots/botdemo/main.go index 383348f8..03de5db8 100644 --- a/cmd/bots/botdemo/main.go +++ b/cmd/bots/botdemo/main.go @@ -158,11 +158,11 @@ func main() { priv, err := mtprotoedge.LoadOrGenerateRSAKey(*rsaPath) if err != nil { - logger.Fatal("加载 RSA key 失败", zap.Error(err)) + logger.Fatal("load RSA key failed", zap.Error(err)) } host, portStr, err := net.SplitHostPort(*addr) if err != nil { - logger.Fatal("解析地址失败", zap.Error(err)) + logger.Fatal("parse address failed", zap.Error(err)) } port, _ := strconv.Atoi(portStr) @@ -196,7 +196,7 @@ func main() { // 3) 定时主动发送。 return runLoop(ctx, api, uploader.NewUploader(api), peer, cfg, logger) }); err != nil { - logger.Fatal("运行失败", zap.Error(err)) + logger.Fatal("run failed", zap.Error(err)) } fmt.Println("已退出") } @@ -286,7 +286,7 @@ func runLoop(ctx context.Context, api *tg.Client, up *uploader.Uploader, peer tg // 立即发第一条(不必等满一个 interval),随后按 ticker 周期发。 if err := send(); err != nil { - logger.Warn("发送失败", zap.Error(err)) + logger.Warn("send failed", zap.Error(err)) } if cfg.count > 0 && seq >= cfg.count { return nil @@ -297,7 +297,7 @@ func runLoop(ctx context.Context, api *tg.Client, up *uploader.Uploader, peer tg return nil case <-ticker.C: if err := send(); err != nil { - logger.Warn("发送失败", zap.Error(err)) + logger.Warn("send failed", zap.Error(err)) continue } if cfg.count > 0 && seq >= cfg.count { diff --git a/cmd/giftcheck/main.go b/cmd/giftcheck/main.go index 228211a3..75aa30ec 100644 --- a/cmd/giftcheck/main.go +++ b/cmd/giftcheck/main.go @@ -91,7 +91,7 @@ func main() { defer func() { _ = logger.Sync() }() priv, err := mtprotoedge.LoadOrGenerateRSAKey(*rsaPath) if err != nil { - logger.Fatal("加载 RSA key 失败", zap.Error(err)) + logger.Fatal("load RSA key failed", zap.Error(err)) } host, portStr, _ := net.SplitHostPort(*addr) port, _ := strconv.Atoi(portStr) @@ -183,7 +183,7 @@ func main() { } return nil }); err != nil { - logger.Fatal("运行失败", zap.Error(err)) + logger.Fatal("run failed", zap.Error(err)) } } diff --git a/cmd/starcheck/main.go b/cmd/starcheck/main.go index d6fd746f..df4dbbc4 100644 --- a/cmd/starcheck/main.go +++ b/cmd/starcheck/main.go @@ -101,11 +101,11 @@ func main() { priv, err := mtprotoedge.LoadOrGenerateRSAKey(*rsaPath) if err != nil { - logger.Fatal("加载 RSA key 失败", zap.Error(err)) + logger.Fatal("load RSA key failed", zap.Error(err)) } host, portStr, err := net.SplitHostPort(*addr) if err != nil { - logger.Fatal("解析地址失败", zap.Error(err)) + logger.Fatal("parse address failed", zap.Error(err)) } port, _ := strconv.Atoi(portStr) @@ -193,7 +193,7 @@ func main() { } return nil }); err != nil { - logger.Fatal("运行失败", zap.Error(err)) + logger.Fatal("run failed", zap.Error(err)) } } diff --git a/cmd/telesrv/log_encoder.go b/cmd/telesrv/log_encoder.go new file mode 100644 index 00000000..047ce737 --- /dev/null +++ b/cmd/telesrv/log_encoder.go @@ -0,0 +1,177 @@ +package main + +import ( + "fmt" + "strings" + "time" + + "go.uber.org/zap/buffer" + "go.uber.org/zap/zapcore" +) + +var compactBufferPool = buffer.NewPool() + +// compactEncoder renders one readable line per entry: short time, level, +// logger, message, then "key=value" pairs in call order — instead of zap's +// stock console encoder, which appends fields as a raw JSON blob and makes +// high-volume RPC logs unreadable at a glance. It writes fields straight +// into a pooled buffer.Buffer (no map/interface{} boxing on the hot path), +// so it stays close to the JSON/console encoders' cost. +type compactEncoder struct { + cfg zapcore.EncoderConfig + buf *buffer.Buffer // accumulated fields from Logger.With(...) +} + +func newCompactEncoder(cfg zapcore.EncoderConfig) *compactEncoder { + return &compactEncoder{cfg: cfg, buf: compactBufferPool.Get()} +} + +func (e *compactEncoder) Clone() zapcore.Encoder { + clone := &compactEncoder{cfg: e.cfg, buf: compactBufferPool.Get()} + clone.buf.AppendString(e.buf.String()) + return clone +} + +func (e *compactEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { + line := compactBufferPool.Get() + + line.AppendString(ent.Time.Format("15:04:05.000")) + line.AppendByte(' ') + + lvl := ent.Level.CapitalString() + line.AppendString(lvl) + for i := len(lvl); i < 5; i++ { + line.AppendByte(' ') + } + line.AppendByte(' ') + + if ent.LoggerName != "" { + line.AppendByte('[') + line.AppendString(ent.LoggerName) + line.AppendString("] ") + } + line.AppendString(ent.Message) + + if e.buf.Len() > 0 { + line.AppendByte(' ') + line.AppendString(e.buf.String()) + } + + if len(fields) > 0 { + tmp := &compactEncoder{cfg: e.cfg, buf: compactBufferPool.Get()} + for _, f := range fields { + f.AddTo(tmp) + } + if tmp.buf.Len() > 0 { + line.AppendByte(' ') + line.AppendString(tmp.buf.String()) + } + tmp.buf.Free() + } + + if ent.Caller.Defined { + line.AppendString(" (") + line.AppendString(ent.Caller.TrimmedPath()) + line.AppendByte(')') + } + + if ent.Stack != "" { + line.AppendByte('\n') + line.AppendString(ent.Stack) + } + + line.AppendByte('\n') + return line, nil +} + +func (e *compactEncoder) addKey(key string) { + if e.buf.Len() > 0 { + e.buf.AppendByte(' ') + } + e.buf.AppendString(key) + e.buf.AppendByte('=') +} + +// appendSafeString keeps "key=value value2" pairs unambiguous: values with +// whitespace, '=' or '"' get quoted, everything else (the vast majority — +// method names, hex ids, enum-ish strings) is written bare. +func (e *compactEncoder) appendSafeString(s string) { + if s == "" || strings.ContainsAny(s, " \t\n=\"") { + e.buf.AppendByte('"') + e.buf.AppendString(strings.ReplaceAll(s, `"`, `\"`)) + e.buf.AppendByte('"') + return + } + e.buf.AppendString(s) +} + +func (e *compactEncoder) AddBool(key string, v bool) { e.addKey(key); e.buf.AppendBool(v) } +func (e *compactEncoder) AddInt64(key string, v int64) { e.addKey(key); e.buf.AppendInt(v) } +func (e *compactEncoder) AddInt(key string, v int) { e.AddInt64(key, int64(v)) } +func (e *compactEncoder) AddInt32(key string, v int32) { e.AddInt64(key, int64(v)) } +func (e *compactEncoder) AddInt16(key string, v int16) { e.AddInt64(key, int64(v)) } +func (e *compactEncoder) AddInt8(key string, v int8) { e.AddInt64(key, int64(v)) } +func (e *compactEncoder) AddUint64(key string, v uint64) { e.addKey(key); e.buf.AppendUint(v) } +func (e *compactEncoder) AddUint(key string, v uint) { e.AddUint64(key, uint64(v)) } +func (e *compactEncoder) AddUint32(key string, v uint32) { e.AddUint64(key, uint64(v)) } +func (e *compactEncoder) AddUint16(key string, v uint16) { e.AddUint64(key, uint64(v)) } +func (e *compactEncoder) AddUint8(key string, v uint8) { e.AddUint64(key, uint64(v)) } +func (e *compactEncoder) AddUintptr(key string, v uintptr) { e.AddUint64(key, uint64(v)) } +func (e *compactEncoder) AddFloat64(key string, v float64) { e.addKey(key); e.buf.AppendFloat(v, 64) } +func (e *compactEncoder) AddFloat32(key string, v float32) { + e.addKey(key) + e.buf.AppendFloat(float64(v), 32) +} +func (e *compactEncoder) AddDuration(key string, v time.Duration) { + e.addKey(key) + e.buf.AppendString(v.String()) +} +func (e *compactEncoder) AddTime(key string, v time.Time) { + e.addKey(key) + e.buf.AppendTime(v, "15:04:05.000") +} +func (e *compactEncoder) AddString(key, v string) { e.addKey(key); e.appendSafeString(v) } +func (e *compactEncoder) AddByteString(key string, v []byte) { + e.addKey(key) + e.appendSafeString(string(v)) +} +func (e *compactEncoder) AddBinary(key string, v []byte) { e.addKey(key); fmt.Fprintf(e.buf, "%x", v) } +func (e *compactEncoder) AddComplex128(key string, v complex128) { + e.addKey(key) + fmt.Fprintf(e.buf, "%v", v) +} +func (e *compactEncoder) AddComplex64(key string, v complex64) { e.AddComplex128(key, complex128(v)) } +func (e *compactEncoder) AddReflected(key string, v interface{}) error { + e.addKey(key) + fmt.Fprintf(e.buf, "%v", v) + return nil +} + +// OpenNamespace isn't used by any log call in this codebase today; nested +// fields would just render as flat top-level keys, which is an acceptable +// degradation for a format whose whole point is staying flat and scannable. +func (e *compactEncoder) OpenNamespace(string) {} + +func (e *compactEncoder) AddArray(key string, m zapcore.ArrayMarshaler) error { + tmp := zapcore.NewMapObjectEncoder() + if err := tmp.AddArray("v", m); err != nil { + e.addKey(key) + e.buf.AppendString("!ERROR") + return err + } + e.addKey(key) + fmt.Fprintf(e.buf, "%v", tmp.Fields["v"]) + return nil +} + +func (e *compactEncoder) AddObject(key string, m zapcore.ObjectMarshaler) error { + tmp := zapcore.NewMapObjectEncoder() + if err := tmp.AddObject("v", m); err != nil { + e.addKey(key) + e.buf.AppendString("!ERROR") + return err + } + e.addKey(key) + fmt.Fprintf(e.buf, "%v", tmp.Fields["v"]) + return nil +} diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index eb0ccb4d..c9fce9b4 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -84,19 +84,22 @@ func main() { defer func() { _ = logger.Sync() }() if err := run(logger); err != nil { - logger.Error("telesrv 退出", zap.Error(err)) - _ = logger.Sync() // os.Exit 跳过 defer;缓冲写需显式 flush 错误日志 + logger.Error("telesrv exiting", zap.Error(err)) + _ = logger.Sync() // os.Exit skips defer; flush buffered log writes explicitly os.Exit(1) } } -// newLogger 构建运行日志器。两项关键改造(相对旧的 zap.NewDevelopment): +// newLogger 构建运行日志器。三项关键改造(相对旧的 zap.NewDevelopment): // - 级别可配(TELESRV_LOG_LEVEL,默认 info):旧版固定 Debug,热路径 65 处 Debug(含 // mtprotoedge 每帧一条)在连接洪峰会刷爆日志。生产/压测用 info 即可,需要时设 debug。 // - 缓冲异步写(BufferedWriteSyncer):旧版每条日志一次 stderr 同步写 + 全局锁,高并发下 // 在日志锁上串行累积——实测连接时 12 个并发 RPC 的 client_info 阶段被拖成 ~1s 惊群 // (mutex profile 91% 竞争在 zap 写)。缓冲后写入批量化,锁持有时间从「每条一次系统调用」 // 降到「攒一批刷一次」。FlushInterval 控制日志可见延迟上界。 +// - compactEncoder(见 log_encoder.go)取代 zap 自带的 console encoder:后者把结构化字段 +// 整体序列化成一坨 JSON 挂在行尾,人眼扫描困难;compactEncoder 直接把字段写成 +// `key=value` 平铺在一行,短时间戳,同时不经 map/interface{} 装箱,性能开销接近原 encoder。 func newLogger() (*zap.Logger, error) { level := zapcore.InfoLevel if v := strings.TrimSpace(os.Getenv("TELESRV_LOG_LEVEL")); v != "" { @@ -108,7 +111,7 @@ func newLogger() (*zap.Logger, error) { WS: zapcore.AddSync(os.Stderr), FlushInterval: 500 * time.Millisecond, } - core := zapcore.NewCore(zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), ws, level) + core := zapcore.NewCore(newCompactEncoder(zap.NewDevelopmentEncoderConfig()), ws, level) return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel), @@ -135,7 +138,7 @@ func newBusinessAutomationOptions(cfg config.Config, online messageapp.BusinessA opts = append(opts, messageapp.WithBusinessAutomationReplyProvider(messageapp.NewAIBusinessAutomationProvider(generator))) logger.Info("Business automation reply provider", zap.String("provider", "ai")) default: - logger.Warn("未知 Business automation AI provider,回退 quick reply 模板", zap.String("provider", cfg.BusinessAIProvider)) + logger.Warn("unknown Business automation AI provider, falling back to quick reply template", zap.String("provider", cfg.BusinessAIProvider)) } return opts } @@ -164,11 +167,11 @@ func newAIComposeOptions(cfg config.Config, limiter aiapp.RateLimiter, premium a Thinking: pc.Thinking, }) if err != nil { - logger.Warn("AI compose provider 已跳过", zap.String("provider", pc.Name), zap.String("kind", pc.Kind), zap.Error(err)) + logger.Warn("AI compose provider skipped", zap.String("provider", pc.Name), zap.String("kind", pc.Kind), zap.Error(err)) continue } providers = append(providers, provider) - logger.Info("AI compose provider 已启用", zap.String("provider", provider.Name()), zap.String("kind", pc.Kind)) + logger.Info("AI compose provider enabled", zap.String("provider", provider.Name()), zap.String("kind", pc.Kind)) } if len(providers) > 0 { opts = append(opts, aiapp.WithProviders(providers...)) @@ -203,16 +206,16 @@ func newTranslationOptions(cfg config.Config, limiter translationapp.RateLimiter OmitTemperature: pc.OmitTemperature, Thinking: pc.Thinking, }) if err != nil { - logger.Warn("translation provider 已跳过", zap.String("provider", pc.Name), zap.Error(err)) + logger.Warn("translation provider skipped", zap.String("provider", pc.Name), zap.Error(err)) continue } providers = append(providers, translationapp.NewAIProvider(provider)) - logger.Info("translation provider 已启用", zap.String("provider", provider.Name()), zap.String("kind", pc.Kind)) + logger.Info("translation provider enabled", zap.String("provider", provider.Name()), zap.String("kind", pc.Kind)) } if len(providers) > 0 { opts = append(opts, translationapp.WithProviders(providers...)) } else if cfg.TranslationEnabled { - logger.Warn("translation 已启用但没有远程 provider;messages.translateText 将返回 TRANSLATIONS_DISABLED") + logger.Warn("translation is enabled but has no remote provider; messages.translateText will return TRANSLATIONS_DISABLED") } return opts } @@ -243,10 +246,10 @@ func startDebugServer(ctx context.Context, addr string, logger *zap.Logger) { srv := &http.Server{Addr: addr, Handler: mux} go func() { - logger.Info("pprof 调试端点已启用", zap.String("addr", addr), + logger.Info("pprof debug endpoint enabled", zap.String("addr", addr), zap.String("hint", "go tool pprof http://"+addr+"/debug/pprof/profile?seconds=30")) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.Warn("pprof 端点退出", zap.Error(err)) + logger.Warn("pprof endpoint exited", zap.Error(err)) } }() go func() { @@ -306,7 +309,7 @@ func run(logger *zap.Logger) error { // tg.Layer 由当前导入的 canonical schema 生成;纳入未来 Layer 后无需 // 在 telesrv 另维护一份常量。 - logger.Info("telesrv 启动", + logger.Info("telesrv starting", zap.String("listen", cfg.ListenAddr), zap.Int("dc", cfg.DC), zap.String("advertise", net.JoinHostPort(cfg.AdvertiseIP, portStr)), @@ -334,7 +337,7 @@ func run(logger *zap.Logger) error { if err != nil { return fmt.Errorf("postgres migrate: %w", err) } - logger.Info("PostgreSQL schema 已迁移", + logger.Info("PostgreSQL schema migrated", zap.Uint("schema_version", migrationStatus.Version), zap.Bool("schema_dirty", migrationStatus.Dirty), zap.Bool("schema_empty", migrationStatus.Empty), @@ -387,7 +390,7 @@ func run(logger *zap.Logger) error { return fmt.Errorf("connect redis: %w", err) } defer func() { _ = rdb.Close() }() - logger.Info("持久化依赖就绪", zap.String("redis", cfg.RedisAddr)) + logger.Info("persistence dependencies ready", zap.String("redis", cfg.RedisAddr)) if cfg.TelegramLoginEnabled { telegramLoginHTTPHandler, err = telegramloginhttp.NewHandler(telegramloginhttp.Config{ Service: telegramLoginService, Tokens: telegramLoginIDTokens, @@ -452,7 +455,7 @@ func run(logger *zap.Logger) error { if err != nil { return fmt.Errorf("init blob backend: %w", err) } - logger.Info("blob backend 就绪", + logger.Info("blob backend ready", zap.String("backend", "localfs"), zap.String("dir", cfg.BlobDir), ) @@ -468,18 +471,18 @@ func run(logger *zap.Logger) error { webPagePreviewOption(cfg), ) if cfg.MapboxToken != "" { - logger.Info("地图缩略图代理已启用", zap.String("provider", "mapbox"), zap.String("cache_dir", cfg.MapTileCacheDir)) + logger.Info("map tile thumbnail proxy enabled", zap.String("provider", "mapbox"), zap.String("cache_dir", cfg.MapTileCacheDir)) } if cfg.ExternalMediaEnable { - logger.Info("外链媒体抓取已启用", zap.Int64("max_bytes", cfg.ExternalMediaMaxBytes), zap.Int("rate_per_min", cfg.ExternalMediaRatePerMin)) + logger.Info("external link media fetching enabled", zap.Int64("max_bytes", cfg.ExternalMediaMaxBytes), zap.Int("rate_per_min", cfg.ExternalMediaRatePerMin)) } if cfg.WebPagePreviewEnable { - logger.Info("链接预览抓取已启用", zap.Int64("max_bytes", cfg.WebPagePreviewMaxBytes), zap.Int("rate_per_min", cfg.WebPagePreviewRatePerMin)) + logger.Info("link preview fetching enabled", zap.Int64("max_bytes", cfg.WebPagePreviewMaxBytes), zap.Int("rate_per_min", cfg.WebPagePreviewRatePerMin)) } if stats, err := filesService.SeedMedia(ctx, cfg.StickerSeedDir, cfg.StickerSeedMaxSets); err != nil { return fmt.Errorf("seed media: %w", err) } else if !stats.Skipped { - logger.Info("媒体种子导入完成", + logger.Info("media seed import complete", zap.String("dir", cfg.StickerSeedDir), zap.Int("reactions", stats.Reactions), zap.Int("sticker_sets", stats.StickerSets), @@ -491,7 +494,7 @@ func run(logger *zap.Logger) error { if stats, err := filesService.SeedAppearance(ctx); err != nil { return fmt.Errorf("seed appearance: %w", err) } else if !stats.Skipped { - logger.Info("外观种子导入完成", + logger.Info("appearance seed import complete", zap.String("source", "default-seed"), zap.Int("wallpapers", stats.Wallpapers), zap.Int("documents", stats.Documents), @@ -501,38 +504,39 @@ func run(logger *zap.Logger) error { if seeded, err := filesService.SeedOfficialSystemAvatar(ctx); err != nil { return fmt.Errorf("seed official system avatar: %w", err) } else if seeded { - logger.Info("官方系统账号头像种子导入完成", zap.Int64("photo_id", domain.OfficialSystemUserPhotoID)) + logger.Info("official system account avatar seed import complete", zap.Int64("photo_id", domain.OfficialSystemUserPhotoID)) } if seeded, err := filesService.SeedBotFatherAvatar(ctx); err != nil { return fmt.Errorf("seed botfather avatar: %w", err) } else if seeded { - logger.Info("BotFather 头像种子导入完成", zap.Int64("photo_id", domain.BotFatherUserPhotoID)) + logger.Info("BotFather avatar seed import complete", zap.Int64("photo_id", domain.BotFatherUserPhotoID)) } if seeded, err := filesService.SeedStickersBotAvatar(ctx); err != nil { return fmt.Errorf("seed stickers bot avatar: %w", err) } else if seeded { - logger.Info("@Stickers 头像种子导入完成", zap.Int64("photo_id", domain.StickersBotUserPhotoID)) + logger.Info("@Stickers avatar seed import complete", zap.Int64("photo_id", domain.StickersBotUserPhotoID)) } if seeded, err := filesService.SeedChatBotAvatar(ctx); err != nil { return fmt.Errorf("seed chatbot avatar: %w", err) } else if seeded { - logger.Info("@ChatBot 头像种子导入完成", zap.Int64("photo_id", domain.ChatBotUserPhotoID)) + logger.Info("@ChatBot avatar seed import complete", zap.Int64("photo_id", domain.ChatBotUserPhotoID)) } if stats, err := filesService.WarmCaches(ctx); err != nil { - logger.Warn("媒体资源缓存预热失败", zap.Error(err)) + logger.Warn("media resource cache warmup failed", zap.Error(err)) } else if stats.StickerSets > 0 || stats.Documents > 0 || stats.Blobs > 0 { - logger.Info("媒体资源缓存预热完成", + logger.Info("media resource cache warmup complete", zap.Int("sticker_sets", stats.StickerSets), zap.Int("documents", stats.Documents), zap.Int("blobs", stats.Blobs), ) } - // 默认 emoji status 系统集:从 animated_emoji 精选合成(幂等,已 seed 的存量 - // 库重启后自动补上);缺失时 premium 用户的 status 选择器会是空的。 + // Default emoji status system set: curated from animated_emoji (idempotent — + // an already-seeded install picks this back up automatically after restart); + // without it, premium users' status picker would be empty. if count, created, err := filesService.EnsureDefaultEmojiStatusSet(ctx); err != nil { - logger.Warn("默认 emoji status 系统集合成失败", zap.Error(err)) + logger.Warn("default emoji status system set assembly failed", zap.Error(err)) } else if created { - logger.Info("默认 emoji status 系统集已合成", zap.Int("documents", count)) + logger.Info("default emoji status system set assembled", zap.Int("documents", count)) } langPackStore := postgres.NewLangPackStore(pool) passwordStore := postgres.NewPasswordStore(pool) @@ -576,16 +580,19 @@ func run(logger *zap.Logger) error { if seeded, err := langPackService.SeedDirectory(ctx, cfg.LangPackSeedDir); err != nil { return fmt.Errorf("seed langpack: %w", err) } else if seeded > 0 { - logger.Info("语言包种子导入完成", zap.String("dir", cfg.LangPackSeedDir), zap.Int("strings", seeded)) + logger.Info("langpack seed import complete", zap.String("dir", cfg.LangPackSeedDir), zap.Int("strings", seeded)) } - // 国家区号目录:把 catalog 固化的官方全量(~235 国)幂等 upsert 进 PG,覆盖迁移里仅 - // seed 的 2 国(US/CN)默认值。否则 countries 表非空,ListCountries 返回那 2 行就会 - // 绕过 catalog,登录页/号码格式只显示 2 国。upsert 失败仅告警不阻断启动(回退旧 2 行)。 + // Country dialing code catalog: idempotently upsert the full official set + // (~235 countries) baked into the catalog into PG, overriding the migration's + // 2-country (US/CN) seed default. Otherwise the countries table is non-empty, + // ListCountries returns just those 2 rows and bypasses the catalog, so the + // login page/number formatting would only ever show 2 countries. An upsert + // failure only warns and doesn't block startup (falls back to the old 2 rows). if cs := catalog.Countries().Countries; len(cs) > 0 { if err := helpStore.UpsertCountries(ctx, cs); err != nil { - logger.Warn("国家区号种子导入失败", zap.Error(err)) + logger.Warn("country dialing code seed import failed", zap.Error(err)) } else { - logger.Info("国家区号种子导入完成", zap.Int("countries", len(cs))) + logger.Info("country dialing code seed import complete", zap.Int("countries", len(cs))) } } @@ -626,7 +633,7 @@ func run(logger *zap.Logger) error { return fmt.Errorf("configure OTP webhook: %w", err) } webhookSender = configured - logger.Info("OTP Webhook 投递已启用", + logger.Info("OTP webhook delivery enabled", zap.Bool("phone", cfg.PhoneCodeDeliveryProvider == "webhook"), zap.Bool("email", (cfg.LoginEmailEnable || cfg.EmailSignupEnable) && cfg.EmailCodeDeliveryProvider == "webhook")) } @@ -732,12 +739,13 @@ func run(logger *zap.Logger) error { defer t.Close() turnService = t } - // 服务端重启恢复:SFU 状态全失,把全部活跃通话的参与者批量置 left(version++), - // 客户端经 checkGroupCall 发现自己 ssrc 消失后自动 rejoin。 + // Restart recovery: SFU state is entirely lost, so mark every active call's + // participants left in bulk (version++); clients notice their own ssrc is + // gone via checkGroupCall and rejoin automatically. if calls, err := groupCallsService.ResetAllParticipants(ctx, int(time.Now().Unix())); err != nil { - logger.Warn("重启清理群通话参与者失败", zap.Error(err)) + logger.Warn("clearing group call participants on restart failed", zap.Error(err)) } else if len(calls) > 0 { - logger.Info("重启清理群通话参与者", zap.Int("calls", len(calls))) + logger.Info("cleared group call participants on restart", zap.Int("calls", len(calls))) } phoneService := phoneapp.NewService(phoneapp.Config{ RingTimeout: cfg.CallRingTimeout, @@ -835,7 +843,7 @@ func run(logger *zap.Logger) error { auth.WithCodeMaxAttempts(cfg.AuthCodeMaxAttempts), auth.WithPhoneCodeDelivery(phoneCodeSender, cfg.PhoneCodeLength), auth.WithOTPDeliveryFailureObserver(func(_ context.Context, request otpdelivery.Request, err error) { - logger.Named("otp").Warn("附加 OTP provider 投递失败,777000 App-code 保持有效", + logger.Named("otp").Warn("supplementary OTP provider delivery failed, 777000 app-code login remains valid", zap.String("delivery_id", request.DeliveryID), zap.String("purpose", string(request.Purpose)), zap.String("channel", string(request.Channel)), @@ -1079,7 +1087,7 @@ func run(logger *zap.Logger) error { OutboundTrackedGlobalMaxBytes: cfg.MTProtoOutboundTrackedGlobalMaxBytes, OutboundWriteGlobalMaxBytes: cfg.MTProtoOutboundWriteGlobalMaxBytes, OnServing: func(_ net.Addr) { - logger.Info("telesrv 服务就绪", + logger.Info("telesrv service ready", zap.String("listen", cfg.ListenAddr), zap.String("advertise", net.JoinHostPort(cfg.AdvertiseIP, portStr)), zap.Int("pid", os.Getpid()), diff --git a/internal/adminapi/server.go b/internal/adminapi/server.go index 443c9d87..de0ae7d5 100644 --- a/internal/adminapi/server.go +++ b/internal/adminapi/server.go @@ -95,9 +95,9 @@ func Start(ctx context.Context, cfg Config, svc Service, log *zap.Logger) (*http ReadHeaderTimeout: 5 * time.Second, } go func() { - log.Info("Admin API 已启用", zap.String("addr", cfg.Addr)) + log.Info("Admin API enabled", zap.String("addr", cfg.Addr)) if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Warn("Admin API 退出", zap.Error(err)) + log.Warn("Admin API exited", zap.Error(err)) } }() go func() { diff --git a/internal/app/files/seed.go b/internal/app/files/seed.go index 8764318a..bbbf3b82 100644 --- a/internal/app/files/seed.go +++ b/internal/app/files/seed.go @@ -49,7 +49,7 @@ func (s *Service) SeedMedia(ctx context.Context, root string, maxRegularSets int // 不存在 → 静默不导入贴纸/reaction」会被埋没(DB 已有旧数据时尤其隐蔽,表现为客户端反复 // 拉取未 seed 的集)。配置 TELESRV_STICKER_SEED_DIR 指向真实导出目录即可。 if s.log != nil { - s.log.Warn("sticker/reaction seed 目录不存在,跳过媒体种子导入(配置 TELESRV_STICKER_SEED_DIR)", + s.log.Warn("sticker/reaction seed directory does not exist, skipping media seed import (set TELESRV_STICKER_SEED_DIR)", zap.String("dir", root), zap.Error(err)) } stats.Skipped = true @@ -121,7 +121,7 @@ func (s *Service) logSeedPhase(phase string, started time.Time, before, after Se if s.log == nil { return } - s.log.Info("媒体种子阶段完成", + s.log.Info("media seed phase complete", zap.String("phase", phase), zap.Duration("elapsed", time.Since(started)), zap.Int("reactions", after.Reactions-before.Reactions), diff --git a/internal/app/files/upload_gc.go b/internal/app/files/upload_gc.go index d86b7183..72e6182b 100644 --- a/internal/app/files/upload_gc.go +++ b/internal/app/files/upload_gc.go @@ -58,10 +58,10 @@ func (w *UploadPartGCWorker) runOnce(ctx context.Context) { } deleted, err := w.files.DeleteExpiredUploadParts(ctx, time.Now().Add(-w.ttl), w.batch) if err != nil { - w.logger.Warn("清理过期 upload_parts 失败", zap.Error(err)) + w.logger.Warn("expired upload_parts cleanup failed", zap.Error(err)) return } if deleted > 0 { - w.logger.Info("清理过期 upload_parts 完成", zap.Int64("deleted", deleted)) + w.logger.Info("expired upload_parts cleanup complete", zap.Int64("deleted", deleted)) } } diff --git a/internal/app/maintenance/retention.go b/internal/app/maintenance/retention.go index 073e784d..03e1b9f9 100644 --- a/internal/app/maintenance/retention.go +++ b/internal/app/maintenance/retention.go @@ -241,15 +241,16 @@ func (w *RetentionWorker) runOutboxPoisonOnce(ctx context.Context) { } outboxDeleted, err := w.outbox.DeleteFailed(ctx, w.outboxPoisonRetention, w.batch) if err != nil { - w.logger.Error("清理 terminal failed dispatch_outbox 失败", + w.logger.Error("cleaning up terminal-failed dispatch_outbox rows failed", zap.String("signal", "dispatch_outbox_poison_cleanup_failed"), zap.Duration("quarantine", w.outboxPoisonRetention), zap.Error(err), ) } else if outboxDeleted > 0 { - // Error 级结构化信号刻意保留:发生 terminal failed 代表确定性编码、事件缺失 - // 或其它不可自动重试故障。任务删除只解冻在线 lane,不会删除 durable event。 - w.logger.Error("terminal failed dispatch_outbox 已结束隔离并释放用户 lane", + // The Error level is intentionally kept: a terminal failure means deterministic + // encoding, a missing event, or some other non-retryable fault. Deleting the row + // only unfreezes the online lane — it does not delete the durable event. + w.logger.Error("terminal-failed dispatch_outbox rows released from quarantine and unfroze their user lane", zap.String("signal", "dispatch_outbox_poison_released"), zap.Int("deleted", outboxDeleted), zap.Duration("quarantine", w.outboxPoisonRetention), @@ -261,26 +262,26 @@ func (w *RetentionWorker) runRetentionOnce(ctx context.Context) { if w.authKeySessionLayers != nil { deleted, err := w.authKeySessionLayers.DeleteExpiredSessionLayers(ctx, w.batch) if err != nil { - w.logger.Warn("回收过期 auth-key session Layer 证据失败", zap.Error(err)) + w.logger.Warn("expired auth-key session layer evidence cleanup failed", zap.Error(err)) } else if deleted > 0 { - w.logger.Info("回收过期 auth-key session Layer 证据完成", zap.Int("deleted", deleted)) + w.logger.Info("expired auth-key session layer evidence cleanup complete", zap.Int("deleted", deleted)) } } if w.loginCodeDeliveries != nil { deleted, err := w.loginCodeDeliveries.DeleteExpiredLoginCodeDeliveries(ctx, time.Now(), w.batch) if err != nil { - w.logger.Warn("回收过期 login-code delivery 回执失败", zap.Error(err)) + w.logger.Warn("expired login-code delivery receipt cleanup failed", zap.Error(err)) } else if deleted > 0 { - w.logger.Info("回收过期 login-code delivery 回执完成", zap.Int("deleted", deleted)) + w.logger.Info("expired login-code delivery receipt cleanup complete", zap.Int("deleted", deleted)) } } if w.tempKeys != nil { expiredBefore := time.Now().Add(-tempAuthKeyExpiryGrace).Unix() tempDeleted, err := w.tempKeys.DeleteExpired(ctx, expiredBefore, w.batch) if err != nil { - w.logger.Warn("回收过期 temp auth key 绑定失败", zap.Error(err)) + w.logger.Warn("expired temp auth key binding cleanup failed", zap.Error(err)) } else if tempDeleted > 0 { - w.logger.Info("回收过期 temp auth key 绑定完成", zap.Int("deleted", tempDeleted)) + w.logger.Info("expired temp auth key binding cleanup complete", zap.Int("deleted", tempDeleted)) } } if w.orphanAuthKeys != nil && w.orphanRetention > 0 { @@ -295,39 +296,40 @@ func (w *RetentionWorker) runRetentionOnce(ctx context.Context) { } else { orphanDeleted, err := w.orphanAuthKeys.DeleteOrphaned(ctx, w.orphanRetention, w.batch, protected) if err != nil { - w.logger.Warn("回收未授权 orphan auth key 失败", zap.Error(err)) + w.logger.Warn("unauthorized orphan auth key cleanup failed", zap.Error(err)) } else if orphanDeleted > 0 { - w.logger.Info("回收未授权 orphan auth key 完成", zap.Int("deleted", orphanDeleted)) + w.logger.Info("unauthorized orphan auth key cleanup complete", zap.Int("deleted", orphanDeleted)) } } } if w.botAPIUpdates != nil { botAPIDeleted, err := w.botAPIUpdates.DeleteDeliveredOrExpired(ctx, botAPIConfirmedGrace, w.botAPIRetention, w.batch) if err != nil { - w.logger.Warn("回收 bot_api_updates 队列失败", zap.Error(err)) + w.logger.Warn("bot_api_updates queue cleanup failed", zap.Error(err)) } else if botAPIDeleted > 0 { - w.logger.Info("回收 bot_api_updates 队列完成", zap.Int("deleted", botAPIDeleted)) + w.logger.Info("bot_api_updates queue cleanup complete", zap.Int("deleted", botAPIDeleted)) } } if w.userUpdates != nil { userDeleted, err := w.userUpdates.DeleteConfirmedPrefix(ctx, w.retention, w.batch) if err != nil { - w.logger.Warn("回收已共同确认的 user_update_events 前缀失败", zap.Error(err)) + w.logger.Warn("jointly-confirmed user_update_events prefix cleanup failed", zap.Error(err)) } else if userDeleted > 0 { - w.logger.Info("回收已共同确认的 user_update_events 前缀完成", zap.Int("deleted", userDeleted)) + w.logger.Info("jointly-confirmed user_update_events prefix cleanup complete", zap.Int("deleted", userDeleted)) } } if w.channelUpdates != nil { channelDeleted, err := w.channelUpdates.DeleteExpiredChannelUpdateEvents(ctx, w.retention, w.batch) if err != nil { - // store 会逐频道隔离坏 gap 后继续本轮;deleted 可能非零,必须同时记录, - // 既不能把全局 pass 伪装成完全失败,也不能吞掉不变量错误。 - w.logger.Warn("回收过期 channel_update_events 存在隔离频道", + // The store quarantines bad gaps per-channel and keeps going for the rest of this + // pass; deleted can be non-zero, so it must be logged alongside the error — the pass + // must not look like a total failure, nor should the invariant error get swallowed. + w.logger.Warn("expired channel_update_events cleanup hit quarantined channels", zap.Int("deleted", channelDeleted), zap.Error(err), ) } else if channelDeleted > 0 { - w.logger.Info("回收过期 channel_update_events 连续前缀完成", zap.Int("deleted", channelDeleted)) + w.logger.Info("expired channel_update_events contiguous-prefix cleanup complete", zap.Int("deleted", channelDeleted)) } } } @@ -360,7 +362,7 @@ func (w *RetentionWorker) touchActiveAuthKeys(ctx context.Context, protected [][ return true } if err := w.activeAuthKeyHeartbeat.TouchActiveRawAuthKeys(ctx, protected); err != nil { - w.logger.Error("刷新 active raw auth key heartbeat 失败,本轮跳过 orphan GC", + w.logger.Error("refreshing active raw auth key heartbeat failed, skipping orphan GC this round", zap.String("signal", "auth_key_heartbeat_failed"), zap.Int("active_keys", len(protected)), zap.Error(err), diff --git a/internal/botapi/server.go b/internal/botapi/server.go index ff3b3df8..7bfcef76 100644 --- a/internal/botapi/server.go +++ b/internal/botapi/server.go @@ -108,9 +108,9 @@ func Start(ctx context.Context, addr string, bots BotsService, users UsersServic return nil, err } go func() { - logger.Info("Bot API 网关已启用", zap.String("addr", addr)) + logger.Info("Bot API gateway enabled", zap.String("addr", addr)) if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { - logger.Warn("Bot API 网关退出", zap.Error(err)) + logger.Warn("Bot API gateway exited", zap.Error(err)) } }() if webhooks, ok := gateway.(GatewayWebhookControl); ok { diff --git a/internal/sfu/pion.go b/internal/sfu/pion.go index 299768e9..d7e884dd 100644 --- a/internal/sfu/pion.go +++ b/internal/sfu/pion.go @@ -175,7 +175,7 @@ func NewPion(cfg PionConfig) (Service, error) { zap.Int("udp_port", cfg.UDPPort), zap.String("advertise_ip", cfg.AdvertiseIP)) if cfg.AdvertiseIP == "127.0.0.1" { - s.log.Warn("TELESRV_SFU_ADVERTISE_IP 为 127.0.0.1:真机 ICE 将无法连接(纯媒体面静默失败),多设备联调必须设为宿主机 LAN IP") + s.log.Warn("TELESRV_SFU_ADVERTISE_IP is 127.0.0.1: real devices' ICE won't be able to connect (media-only silent failure); multi-device testing requires the host's LAN IP") } return s, nil } diff --git a/internal/turnsrv/turnsrv.go b/internal/turnsrv/turnsrv.go index c3f8ee3f..06a835f4 100644 --- a/internal/turnsrv/turnsrv.go +++ b/internal/turnsrv/turnsrv.go @@ -139,7 +139,7 @@ func New(cfg Config) (Service, error) { zap.Int("relay_min_port", cfg.RelayMinPort), zap.Int("relay_max_port", cfg.RelayMaxPort)) if cfg.AdvertiseIP == "127.0.0.1" { - cfg.Logger.Warn("TELESRV_TURN_ADVERTISE_IP 为 127.0.0.1:真机拿到的 relay candidate 不可达(跨网媒体面静默失败),跨设备通话必须设为客户端可达 IP") + cfg.Logger.Warn("TELESRV_TURN_ADVERTISE_IP is 127.0.0.1: real devices get an unreachable relay candidate (cross-network media silently fails); cross-device calls require a client-reachable IP") } return &pionTURN{cfg: cfg, server: server}, nil }