优化结构
This commit is contained in:
parent
ba1c73fa66
commit
690a2d71ca
|
|
@ -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,19 +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{}{}
|
||||||
}
|
}
|
||||||
// Files stay structured JSON for searching and the log viewer. The console
|
consoleCore := &consoleSummaryCore{level: levelEnabler, output: zapcore.AddSync(os.Stdout), prefix: options.Prefix, fileOnly: fileOnly}
|
||||||
// gets a compact human-readable encoder so entries do not become one long
|
core = zapcore.NewTee(fileCore, consoleCore)
|
||||||
// JSON line that extends far beyond the terminal viewport.
|
|
||||||
consoleEncoderConfig := zap.NewProductionEncoderConfig()
|
|
||||||
consoleEncoderConfig.EncodeTime = func(value time.Time, output zapcore.PrimitiveArrayEncoder) {
|
|
||||||
output.AppendString(options.Prefix + value.Format("2006-01-02 15:04:05.000"))
|
|
||||||
}
|
|
||||||
consoleEncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder
|
|
||||||
consoleEncoderConfig.EncodeCaller = zapcore.FullCallerEncoder
|
|
||||||
consoleEncoderConfig.ConsoleSeparator = " "
|
|
||||||
consoleEncoder := zapcore.NewConsoleEncoder(consoleEncoderConfig)
|
|
||||||
consoleCore := zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), levelEnabler)
|
|
||||||
core = zapcore.NewTee(fileCore, &moduleFilterCore{Core: consoleCore, fileOnly: fileOnly})
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,29 @@ func TestConsoleEncoderDoesNotChangeJSONFileEncoder(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue