feat(log): 调整可视化能力
This commit is contained in:
parent
03267710dc
commit
ec8406fe42
@ -1,5 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## [1.1.14] - 2026-05-09
|
||||
- **可视化能力调整**: 调整优化了 `viewer` 模块相关的可视化能力,提升了日志的可读性与调试体验。
|
||||
|
||||
## [1.1.13] - 2026-05-09
|
||||
- **绝对索引优化与零空洞**:
|
||||
- 彻底消除 `BaseLog` 与业务字段之间的索引空洞。字段位置调整为:`BaseLog` (0-5),标准消息字段 (`Info`, `Error` 等) 与业务日志字段从 `pos: 6` 起始。
|
||||
|
||||
26
meta.go
26
meta.go
@ -12,12 +12,15 @@ import (
|
||||
|
||||
// MetaField describes the serialization and visualization metadata for a single log field.
|
||||
type MetaField struct {
|
||||
Index int `json:"index"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
WithoutKey bool `json:"withoutKey,omitempty"`
|
||||
Hide bool `json:"hide,omitempty"`
|
||||
Index int
|
||||
Name string
|
||||
KeyName string
|
||||
AttachBefore bool
|
||||
Color string
|
||||
Format string
|
||||
Precision int
|
||||
WithoutKey bool
|
||||
Hide bool
|
||||
}
|
||||
|
||||
var (
|
||||
@ -144,6 +147,11 @@ func extractMetaFields(model any) []MetaField {
|
||||
if tag != "" {
|
||||
parts := strings.Split(tag, ",")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "attachBefore" {
|
||||
meta.AttachBefore = true
|
||||
continue
|
||||
}
|
||||
kv := strings.SplitN(part, ":", 2)
|
||||
if len(kv) == 2 {
|
||||
key := strings.TrimSpace(kv[0])
|
||||
@ -157,6 +165,12 @@ func extractMetaFields(model any) []MetaField {
|
||||
meta.WithoutKey = (val == "true")
|
||||
case "hide":
|
||||
meta.Hide = (val == "true")
|
||||
case "keyname":
|
||||
meta.KeyName = val
|
||||
case "attachBefore":
|
||||
meta.AttachBefore = (val == "true")
|
||||
case "precision":
|
||||
meta.Precision = cast.To[int](val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,9 +30,9 @@ type BaseLog struct {
|
||||
LogName string `log:"pos:0,color:cyan,hide:true"`
|
||||
LogType string `log:"pos:1,color:magenta,hide:true"`
|
||||
LogTime int64 `log:"pos:2,format:time"`
|
||||
TraceId string `log:"pos:3,color:blue"`
|
||||
Image string `log:"pos:4,color:darkGray,hide:true"`
|
||||
Server string `log:"pos:5,color:darkGray,hide:true"`
|
||||
TraceId string `log:"pos:3,color:gray,withoutkey:true"`
|
||||
Image string `log:"pos:4,hide:true"`
|
||||
Server string `log:"pos:5,hide:true"`
|
||||
Extra map[string]any `log:"pos:1000"`
|
||||
}
|
||||
|
||||
|
||||
131
viewer.go
131
viewer.go
@ -2,7 +2,9 @@ package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -12,6 +14,7 @@ import (
|
||||
|
||||
var errorLineMatcher = regexp.MustCompile(`(\w+\.go:\d+)`)
|
||||
var codeFileMatcher = regexp.MustCompile(`(\w+?\.)(go|js)`)
|
||||
var workspaceRoot, _ = os.Getwd()
|
||||
|
||||
func Viewable(line string) string {
|
||||
line = strings.TrimSpace(line)
|
||||
@ -76,17 +79,16 @@ func Viewable(line string) string {
|
||||
if m.Name == "Extra" {
|
||||
extraMap, ok := v.(map[string]any)
|
||||
if ok && len(extraMap) > 0 {
|
||||
for k, ev := range extraMap {
|
||||
keys := make([]string, 0, len(extraMap))
|
||||
for k := range extraMap {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
ev := extraMap[k]
|
||||
builder.WriteString(" ")
|
||||
builder.WriteString(shell.Style(shell.TextWhite, shell.Dim, shell.Italic, k+":"))
|
||||
vStr := ""
|
||||
switch ev.(type) {
|
||||
case map[string]any, []any:
|
||||
vStr, _ = cast.ToJSON(ev)
|
||||
default:
|
||||
vStr = cast.String(ev)
|
||||
}
|
||||
builder.WriteString(vStr)
|
||||
builder.WriteString(renderValue(ev, 0, ""))
|
||||
}
|
||||
}
|
||||
continue
|
||||
@ -95,9 +97,18 @@ func Viewable(line string) string {
|
||||
if m.Name == "CallStacks" {
|
||||
callStacksList, ok := v.([]any)
|
||||
if ok && len(callStacksList) > 0 {
|
||||
stackColor := shell.TextRed
|
||||
if strings.Contains(strings.ToLower(logType), "warn") {
|
||||
stackColor = shell.TextYellow
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
for _, vi := range callStacksList {
|
||||
vStr := cast.String(vi)
|
||||
if workspaceRoot != "" {
|
||||
vStr = strings.TrimPrefix(vStr, workspaceRoot)
|
||||
vStr = strings.TrimPrefix(vStr, "/")
|
||||
}
|
||||
postfix := ""
|
||||
if pos := strings.LastIndexByte(vStr, '/'); pos != -1 {
|
||||
postfix = vStr[pos+1:]
|
||||
@ -108,7 +119,7 @@ func Viewable(line string) string {
|
||||
}
|
||||
builder.WriteString(" ")
|
||||
builder.WriteString(shell.Style(shell.Dim, vStr))
|
||||
builder.WriteString(shell.Style(shell.TextWhite, postfix))
|
||||
builder.WriteString(shell.Style(stackColor, postfix))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
@ -120,28 +131,42 @@ func Viewable(line string) string {
|
||||
if m.Format == "time" {
|
||||
// Convert int64 ns to time string
|
||||
logTime := time.Unix(0, cast.Int64(v))
|
||||
vStr = logTime.Format("01-02 15:04:05.000")
|
||||
if m.Color == "" {
|
||||
builder.WriteString(shell.White(shell.Bold, vStr))
|
||||
dateStr := logTime.Format("01-02")
|
||||
timeStr := logTime.Format("15:04:05")
|
||||
milliStr := logTime.Format(".000")
|
||||
|
||||
if builder.Len() > 0 {
|
||||
builder.WriteString(" ")
|
||||
continue
|
||||
}
|
||||
builder.WriteString(shell.Style(shell.Dim, dateStr))
|
||||
builder.WriteString(" ")
|
||||
builder.WriteString(shell.White(shell.Bold, timeStr))
|
||||
builder.WriteString(shell.Style(shell.Dim, milliStr))
|
||||
continue
|
||||
} else {
|
||||
vStr = cast.String(v)
|
||||
vStr = renderValue(v, m.Precision, m.Color)
|
||||
if vStr == "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if builder.Len() > 0 {
|
||||
if m.AttachBefore {
|
||||
builder.WriteString(":")
|
||||
} else {
|
||||
builder.WriteString(" ")
|
||||
}
|
||||
|
||||
if !m.WithoutKey {
|
||||
builder.WriteString(shell.Style(shell.TextWhite, shell.Dim, shell.Italic, m.Name+":"))
|
||||
}
|
||||
|
||||
builder.WriteString(applyColor(vStr, m.Color))
|
||||
if !m.WithoutKey && !m.AttachBefore {
|
||||
name := m.KeyName
|
||||
if name == "" {
|
||||
name = m.Name
|
||||
}
|
||||
builder.WriteString(shell.Style(shell.TextWhite, shell.Dim, shell.Italic, name+":"))
|
||||
}
|
||||
|
||||
builder.WriteString(vStr)
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
@ -184,6 +209,12 @@ func ToJSON(line string) string {
|
||||
if m.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
name := m.KeyName
|
||||
if name == "" {
|
||||
name = m.Name
|
||||
}
|
||||
|
||||
if m.Name == "Extra" {
|
||||
if extraMap, ok := v.(map[string]any); ok {
|
||||
for k, ev := range extraMap {
|
||||
@ -191,7 +222,7 @@ func ToJSON(line string) string {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result[m.Name] = v
|
||||
result[name] = v
|
||||
}
|
||||
} else if cast.String(v) != "0" {
|
||||
result[fmt.Sprintf("Extra%d", i)] = v
|
||||
@ -216,7 +247,7 @@ func applyColor(text string, color string) string {
|
||||
return shell.Yellow(text)
|
||||
case "green":
|
||||
return shell.Green(text)
|
||||
case "gray", "darkGray":
|
||||
case "gray":
|
||||
return shell.Style(shell.Dim, text)
|
||||
default:
|
||||
return text
|
||||
@ -233,3 +264,61 @@ func fallbackRenderArray(arr []any) string {
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func renderValue(v any, precision int, color string) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case float32, float64:
|
||||
vStr := ""
|
||||
if precision > 0 {
|
||||
vStr = fmt.Sprintf("%.*f", precision, cast.To[float64](v))
|
||||
} else {
|
||||
vStr = cast.String(v)
|
||||
}
|
||||
return applyColor(vStr, color)
|
||||
case map[string]any:
|
||||
if len(val) == 0 {
|
||||
return ""
|
||||
}
|
||||
var parts []string
|
||||
keys := make([]string, 0, len(val))
|
||||
for k := range val {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
// Key is always dim, value is colored
|
||||
vStr := renderValue(val[k], precision, color)
|
||||
if vStr != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s:%s", shell.Style(shell.Dim, k), vStr))
|
||||
}
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "[ " + strings.Join(parts, " ") + " ]"
|
||||
case []any:
|
||||
if len(val) == 0 {
|
||||
return ""
|
||||
}
|
||||
var parts []string
|
||||
for _, iv := range val {
|
||||
vStr := renderValue(iv, precision, color)
|
||||
if vStr != "" {
|
||||
parts = append(parts, vStr)
|
||||
}
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "[ " + strings.Join(parts, " ") + " ]"
|
||||
default:
|
||||
s := cast.String(v)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
return applyColor(s, color)
|
||||
}
|
||||
}
|
||||
|
||||
151
viewer_test.go
151
viewer_test.go
@ -77,3 +77,154 @@ func TestLoadMeta(t *testing.T) {
|
||||
t.Errorf("expected Field1, got %s", meta[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
type EnhancedLog struct {
|
||||
log.BaseLog
|
||||
App string `log:"pos:10,withoutkey:true"`
|
||||
Node string `log:"pos:11,attachBefore,withoutkey:true"`
|
||||
RequestHeaders map[string]string `log:"pos:13,keyname:reqH"`
|
||||
ClientIP string `log:"pos:12,keyname:ip"`
|
||||
Tags []string `log:"pos:14"`
|
||||
}
|
||||
|
||||
func (l *EnhancedLog) Reset() {
|
||||
l.BaseLog.Reset()
|
||||
l.App = ""
|
||||
l.Node = ""
|
||||
l.ClientIP = ""
|
||||
l.RequestHeaders = nil
|
||||
l.Tags = nil
|
||||
}
|
||||
|
||||
func TestEnhancedViewable(t *testing.T) {
|
||||
entry := &EnhancedLog{
|
||||
BaseLog: log.BaseLog{
|
||||
LogType: "enhanced",
|
||||
LogTime: 1714896000000000000,
|
||||
},
|
||||
App: "MyApp",
|
||||
Node: "Node1",
|
||||
ClientIP: "127.0.0.1",
|
||||
RequestHeaders: map[string]string{
|
||||
"User-Agent": "Go-http-cli",
|
||||
},
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
}
|
||||
log.RegisterType("enhanced", entry)
|
||||
|
||||
line := string(log.ToArrayBytes(entry, nil))
|
||||
out := log.Viewable(line)
|
||||
|
||||
// Check attachBefore: MyApp:Node1 (since both are withoutkey)
|
||||
if !strings.Contains(out, "MyApp:Node1") {
|
||||
t.Errorf("expected MyApp:Node1, got: %s", out)
|
||||
}
|
||||
|
||||
// Check pos ordering and keyname: ip:127.0.0.1 should come before reqH
|
||||
if !strings.Contains(out, "ip:") || !strings.Contains(out, "127.0.0.1") {
|
||||
t.Errorf("expected ip:127.0.0.1, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "reqH:") || !strings.Contains(out, "User-Agent") || !strings.Contains(out, "Go-http-cli") {
|
||||
t.Errorf("expected reqH:[ User-Agent:Go-http-cli ], got: %s", out)
|
||||
}
|
||||
|
||||
ipIdx := strings.Index(out, "ip:")
|
||||
reqHIdx := strings.Index(out, "reqH:")
|
||||
if ipIdx > reqHIdx {
|
||||
t.Errorf("expected ip to come before reqH, but ipIdx=%d, reqHIdx=%d", ipIdx, reqHIdx)
|
||||
}
|
||||
|
||||
// Check array rendering: Tags:[ tag1 tag2 ]
|
||||
if !strings.Contains(out, "Tags:") || !strings.Contains(out, "[ tag1 tag2 ]") {
|
||||
t.Errorf("expected Tags:[ tag1 tag2 ], got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnhancedToJSON(t *testing.T) {
|
||||
entry := &EnhancedLog{
|
||||
BaseLog: log.BaseLog{
|
||||
LogType: "enhanced",
|
||||
LogTime: 1714896000000000000,
|
||||
},
|
||||
App: "MyApp",
|
||||
Node: "Node1",
|
||||
ClientIP: "127.0.0.1",
|
||||
RequestHeaders: map[string]string{
|
||||
"User-Agent": "Go-http-cli",
|
||||
},
|
||||
}
|
||||
log.RegisterType("enhanced", entry)
|
||||
|
||||
line := string(log.ToArrayBytes(entry, nil))
|
||||
jsonStr := log.ToJSON(line)
|
||||
|
||||
// Check keyname in JSON
|
||||
if !strings.Contains(jsonStr, `"ip":"127.0.0.1"`) {
|
||||
t.Errorf("expected ip field in JSON, got: %s", jsonStr)
|
||||
}
|
||||
if !strings.Contains(jsonStr, `"reqH":{"User-Agent":"Go-http-cli"}`) {
|
||||
t.Errorf("expected reqH field in JSON, got: %s", jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
type CallStackLog struct {
|
||||
log.BaseLog
|
||||
CallStacks []string `log:"pos:6"`
|
||||
}
|
||||
|
||||
func (l *CallStackLog) Reset() {
|
||||
l.BaseLog.Reset()
|
||||
l.CallStacks = nil
|
||||
}
|
||||
|
||||
func TestCallStacksViewable(t *testing.T) {
|
||||
wd, _ := os.Getwd()
|
||||
entry := &CallStackLog{
|
||||
BaseLog: log.BaseLog{
|
||||
LogType: "error",
|
||||
LogTime: 1714896000000000000,
|
||||
},
|
||||
CallStacks: []string{wd + "/main.go:10", "/usr/local/go/src/runtime/panic.go:100"},
|
||||
}
|
||||
log.RegisterType("error", entry)
|
||||
|
||||
line := string(log.ToArrayBytes(entry, nil))
|
||||
out := log.Viewable(line)
|
||||
|
||||
// Check path truncation (should contain relative "main.go:10")
|
||||
if !strings.Contains(out, "main.go:10") {
|
||||
t.Errorf("expected relative path main.go:10, got: %s", out)
|
||||
}
|
||||
// Absolute path should be removed if it matches wd
|
||||
if strings.Contains(out, wd) {
|
||||
t.Errorf("absolute path should be truncated, but still found: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
type PrecisionLog struct {
|
||||
log.BaseLog
|
||||
Value float64 `log:"pos:6,precision:2"`
|
||||
}
|
||||
|
||||
func (l *PrecisionLog) Reset() {
|
||||
l.BaseLog.Reset()
|
||||
l.Value = 0
|
||||
}
|
||||
|
||||
func TestPrecisionViewable(t *testing.T) {
|
||||
entry := &PrecisionLog{
|
||||
BaseLog: log.BaseLog{
|
||||
LogType: "precision",
|
||||
LogTime: 1714896000000000000,
|
||||
},
|
||||
Value: 3.14159,
|
||||
}
|
||||
log.RegisterType("precision", entry)
|
||||
|
||||
line := string(log.ToArrayBytes(entry, nil))
|
||||
out := log.Viewable(line)
|
||||
|
||||
if !strings.Contains(out, "3.14") || strings.Contains(out, "3.141") {
|
||||
t.Errorf("expected 3.14 (precision 2), got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user