Compare commits

..

2 Commits

Author SHA1 Message Date
Yvan 690a2d71ca 优化结构 2026-08-21 17:09:50 +08:00
Yvan ba1c73fa66 优化结构 2026-08-21 16:52:37 +08:00
5 changed files with 139 additions and 41 deletions

View File

@ -128,7 +128,10 @@ admin:
access_resp_data: true access_resp_data: true
access_req_headers: true access_req_headers: true
access_log_max_bytes: 1024 access_log_max_bytes: 1024
file_only_modules: [] # High-volume request and SQL details remain available in logs/http and logs/sql.
file_only_modules:
- http
- sql
cors: cors:
mode: whitelist mode: whitelist
whitelist: [] whitelist: []

View File

@ -6,7 +6,6 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"sort"
"strings" "strings"
"time" "time"
@ -79,28 +78,9 @@ func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessContro
} }
httpx.Fail(c, "请求的接口不存在") httpx.Fail(c, "请求的接口不存在")
}) })
logRegisteredRoutes(engine, logger)
return engine return engine
} }
func logRegisteredRoutes(engine *gin.Engine, logger *slog.Logger) {
if logger == nil {
return
}
routes := append([]gin.RouteInfo(nil), engine.Routes()...)
sort.Slice(routes, func(i, j int) bool {
if routes[i].Path == routes[j].Path {
return routes[i].Method < routes[j].Method
}
return routes[i].Path < routes[j].Path
})
systemLogger := logger.With("mod", "system")
for _, route := range routes {
systemLogger.Info("router registered", "method", route.Method, "path", route.Path)
}
systemLogger.Info("router register success", "route_count", len(routes))
}
func NewGinServer(c *conf.Server, engine *gin.Engine) *kratoshttp.Server { func NewGinServer(c *conf.Server, engine *gin.Engine) *kratoshttp.Server {
network, address := "tcp", ":8000" network, address := "tcp", ":8000"
if c != nil && c.Http != nil { if c != nil && c.Http != nil {

View File

@ -6,6 +6,7 @@ import (
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -151,36 +152,111 @@ func (l *ReloadableLogger) Close() {
} }
} }
type moduleFilterCore struct { type consoleSummaryCore struct {
zapcore.Core level zapcore.LevelEnabler
output zapcore.WriteSyncer
prefix string
fileOnly map[string]struct{} fileOnly map[string]struct{}
mod string fields []zapcore.Field
} }
func (c *moduleFilterCore) With(fields []zapcore.Field) zapcore.Core { func (c *consoleSummaryCore) Enabled(level zapcore.Level) bool { return c.level.Enabled(level) }
mod := c.mod
if value := moduleField(fields); value != "" { func (c *consoleSummaryCore) With(fields []zapcore.Field) zapcore.Core {
mod = value inherited := append([]zapcore.Field(nil), c.fields...)
} inherited = append(inherited, fields...)
return &moduleFilterCore{Core: c.Core.With(fields), fileOnly: c.fileOnly, mod: mod} return &consoleSummaryCore{level: c.level, output: c.output, prefix: c.prefix, fileOnly: c.fileOnly, fields: inherited}
} }
func (c *moduleFilterCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry { func (c *consoleSummaryCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if c.Enabled(entry.Level) { if c.Enabled(entry.Level) {
return checked.AddCore(entry, c) return checked.AddCore(entry, c)
} }
return checked return checked
} }
func (c *moduleFilterCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { func (c *consoleSummaryCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
mod := c.mod allFields := append([]zapcore.Field(nil), c.fields...)
if value := moduleField(fields); value != "" { allFields = append(allFields, fields...)
mod = value module := moduleField(allFields)
} if _, excluded := c.fileOnly[module]; excluded {
if _, excluded := c.fileOnly[mod]; excluded {
return nil return nil
} }
return c.Core.Write(entry, fields) _, err := c.output.Write([]byte(formatConsoleEntry(entry, module, allFields, c.prefix)))
return err
}
func (c *consoleSummaryCore) Sync() error { return c.output.Sync() }
var hiddenConsoleFields = map[string]struct{}{
"mod": {}, "service.id": {}, "service.name": {}, "service.version": {},
"node": {}, "app_id": {}, "env": {}, "trace_id": {}, "span_id": {},
}
func formatConsoleEntry(entry zapcore.Entry, module string, fields []zapcore.Field, prefix string) string {
values := zapcore.NewMapObjectEncoder()
for _, field := range fields {
field.AddTo(values)
}
keys := make([]string, 0, len(values.Fields))
for key, value := range values.Fields {
if _, hidden := hiddenConsoleFields[key]; hidden || consoleValueEmpty(value) {
continue
}
keys = append(keys, key)
}
sort.Strings(keys)
caller := ""
if entry.Caller.Defined {
caller = filepath.Base(entry.Caller.File) + fmt.Sprintf(":%d", entry.Caller.Line)
}
if module == "" {
module = "app"
}
parts := []string{
prefix + entry.Time.Format("15:04:05.000"),
consoleLevel(entry.Level),
fmt.Sprintf("%-10s", module),
}
if caller != "" {
parts = append(parts, fmt.Sprintf("%-24s", caller))
}
parts = append(parts, entry.Message)
for _, key := range keys {
parts = append(parts, key+"="+formatConsoleValue(values.Fields[key]))
}
return strings.Join(parts, " ") + "\n"
}
func consoleLevel(level zapcore.Level) string {
label := strings.ToUpper(level.String())
color := "\x1b[36m"
switch {
case level >= zapcore.ErrorLevel:
color = "\x1b[31m"
case level == zapcore.WarnLevel:
color = "\x1b[33m"
case level == zapcore.InfoLevel:
color = "\x1b[32m"
}
return color + fmt.Sprintf("%-5s", label) + "\x1b[0m"
}
func consoleValueEmpty(value any) bool {
return value == nil || value == ""
}
func formatConsoleValue(value any) string {
text := fmt.Sprint(value)
text = strings.Join(strings.Fields(text), " ")
if len(text) > 180 {
text = text[:177] + "..."
}
if strings.ContainsAny(text, " \t\r\n") {
return fmt.Sprintf("%q", text)
}
return text
} }
func moduleField(fields []zapcore.Field) string { func moduleField(fields []zapcore.Field) string {
@ -433,8 +509,8 @@ func newZapHandler(root, filename string, options Options, errorSink *errorSinkS
for _, module := range options.FileOnlyModules { for _, module := range options.FileOnlyModules {
fileOnly[module] = struct{}{} fileOnly[module] = struct{}{}
} }
consoleCore := zapcore.NewCore(outputEncoder.Clone(), zapcore.AddSync(os.Stdout), levelEnabler) consoleCore := &consoleSummaryCore{level: levelEnabler, output: zapcore.AddSync(os.Stdout), prefix: options.Prefix, fileOnly: fileOnly}
core = zapcore.NewTee(fileCore, &moduleFilterCore{Core: consoleCore, fileOnly: fileOnly}) core = zapcore.NewTee(fileCore, consoleCore)
} }
routed := &routedFileCore{base: core, encoder: outputEncoder.Clone(), level: levelEnabler, root: root, retentionDay: options.RetentionDay, state: &routedFileState{writers: map[string]*DailyWriter{}}, errorSink: errorSink} routed := &routedFileCore{base: core, encoder: outputEncoder.Clone(), level: levelEnabler, root: root, retentionDay: options.RetentionDay, state: &routedFileState{writers: map[string]*DailyWriter{}}, errorSink: errorSink}
zapLogger := zap.New(routed) zapLogger := zap.New(routed)

View File

@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"time"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
@ -61,6 +62,44 @@ func TestZapHandlerRoutesHTTPAndErrorLogs(t *testing.T) {
} }
} }
func TestConsoleEncoderDoesNotChangeJSONFileEncoder(t *testing.T) {
config := zap.NewProductionEncoderConfig()
config.EncodeTime = zapcore.RFC3339NanoTimeEncoder
fileEncoder := zapcore.NewJSONEncoder(config)
entry := zapcore.Entry{Level: zapcore.InfoLevel, Time: time.Date(2026, 8, 21, 16, 30, 0, 0, time.Local), Message: "started"}
buffer, err := fileEncoder.EncodeEntry(entry, nil)
if err != nil {
t.Fatal(err)
}
defer buffer.Free()
if !strings.HasPrefix(buffer.String(), "{") || !strings.Contains(buffer.String(), `"msg":"started"`) {
t.Fatalf("file encoder is no longer JSON: %s", buffer.String())
}
}
func TestConsoleSummaryHidesInfrastructureNoise(t *testing.T) {
entry := zapcore.Entry{
Level: zapcore.InfoLevel,
Time: time.Date(2026, 8, 21, 16, 50, 53, 145000000, time.Local),
Message: "register swagger handler",
Caller: zapcore.NewEntryCaller(0, `D:\workspace\kra\internal\server\swagger.go`, 55, true),
}
line := formatConsoleEntry(entry, "system", []zapcore.Field{
zap.String("service.id", "DESKTOP"), zap.String("app_id", "kra"),
zap.String("trace_id", ""), zap.String("path", "/swagger/*any"),
}, "[kra] ")
for _, want := range []string{"[kra] 16:50:53.145", "INFO", "system", "swagger.go:55", "register swagger handler", "path=/swagger/*any"} {
if !strings.Contains(line, want) {
t.Fatalf("console line %q does not contain %q", line, want)
}
}
for _, unwanted := range []string{"service.id", "DESKTOP", "app_id", "trace_id", `D:\workspace`} {
if strings.Contains(line, unwanted) {
t.Fatalf("console line still contains %q: %s", unwanted, line)
}
}
}
func TestZapHandlerRecordsEveryErrorThroughSink(t *testing.T) { func TestZapHandlerRecordsEveryErrorThroughSink(t *testing.T) {
root := t.TempDir() root := t.TempDir()
var entries []ErrorEntry var entries []ErrorEntry

View File

@ -257,7 +257,7 @@
jwt: { signingKey: '******', expiresTime: '168h', bufferTime: '24h', issuer: 'kra' }, jwt: { signingKey: '******', expiresTime: '168h', bufferTime: '24h', issuer: 'kra' },
captcha: { keyLong: 6, imgWidth: 240, imgHeight: 80, storeExpiration: '3m' }, captcha: { keyLong: 6, imgWidth: 240, imgHeight: 80, storeExpiration: '3m' },
local: { storePath: 'uploads/file', pathPrefix: 'uploads/file' }, media: { sessionTtl: 24, maxFileSize: 0, chunkDir: 'uploads/chunks' }, local: { storePath: 'uploads/file', pathPrefix: 'uploads/file' }, media: { sessionTtl: 24, maxFileSize: 0, chunkDir: 'uploads/chunks' },
zap: { level: 'info', prefix: '[kra] ', format: 'json', director: 'logs', encode_level: 'LowercaseLevelEncoder', stacktrace_key: 'stacktrace', show_line: true, log_in_console: true, retention_day: 7, access_req_body: true, access_resp_data: true, access_req_headers: true, access_log_max_bytes: 1024, file_only_modules: [] }, zap: { level: 'info', prefix: '[kra] ', format: 'json', director: 'logs', encode_level: 'LowercaseLevelEncoder', stacktrace_key: 'stacktrace', show_line: true, log_in_console: true, retention_day: 7, access_req_body: true, access_resp_data: true, access_req_headers: true, access_log_max_bytes: 1024, file_only_modules: ['http', 'sql'] },
cors: { mode: 'whitelist', whitelist: [] }, app: { node: '', app_id: 'kra', env: 'development' }, cors: { mode: 'whitelist', whitelist: [] }, app: { node: '', app_id: 'kra', env: 'development' },
system: { useRedis: false, useMultipoint: false, useStrictAuth: false, disableAutoMigrate: false, useMongo: false, addr: 8000, iplimitCount: 0, iplimitTime: 0 }, system: { useRedis: false, useMultipoint: false, useStrictAuth: false, disableAutoMigrate: false, useMongo: false, addr: 8000, iplimitCount: 0, iplimitTime: 0 },
storage: { type: 'local', qiniu: {}, aliyun_oss: {}, huawei_obs: {}, tencent_cos: {}, aws_s3: {}, cloudflare_r2: {}, minio: {} } storage: { type: 'local', qiniu: {}, aliyun_oss: {}, huawei_obs: {}, tencent_cos: {}, aws_s3: {}, cloudflare_r2: {}, minio: {} }