feat(log): 调整可视化能力
This commit is contained in:
parent
03267710dc
commit
ec8406fe42
@ -1,5 +1,8 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.1.14] - 2026-05-09
|
||||||
|
- **可视化能力调整**: 调整优化了 `viewer` 模块相关的可视化能力,提升了日志的可读性与调试体验。
|
||||||
|
|
||||||
## [1.1.13] - 2026-05-09
|
## [1.1.13] - 2026-05-09
|
||||||
- **绝对索引优化与零空洞**:
|
- **绝对索引优化与零空洞**:
|
||||||
- 彻底消除 `BaseLog` 与业务字段之间的索引空洞。字段位置调整为:`BaseLog` (0-5),标准消息字段 (`Info`, `Error` 等) 与业务日志字段从 `pos: 6` 起始。
|
- 彻底消除 `BaseLog` 与业务字段之间的索引空洞。字段位置调整为:`BaseLog` (0-5),标准消息字段 (`Info`, `Error` 等) 与业务日志字段从 `pos: 6` 起始。
|
||||||
|
|||||||
28
meta.go
28
meta.go
@ -12,12 +12,15 @@ import (
|
|||||||
|
|
||||||
// MetaField describes the serialization and visualization metadata for a single log field.
|
// MetaField describes the serialization and visualization metadata for a single log field.
|
||||||
type MetaField struct {
|
type MetaField struct {
|
||||||
Index int `json:"index"`
|
Index int
|
||||||
Name string `json:"name"`
|
Name string
|
||||||
Color string `json:"color,omitempty"`
|
KeyName string
|
||||||
Format string `json:"format,omitempty"`
|
AttachBefore bool
|
||||||
WithoutKey bool `json:"withoutKey,omitempty"`
|
Color string
|
||||||
Hide bool `json:"hide,omitempty"`
|
Format string
|
||||||
|
Precision int
|
||||||
|
WithoutKey bool
|
||||||
|
Hide bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -48,7 +51,7 @@ func RegisterType(logType string, model any) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
panic("log model must implement Reset() method: " + t.Name())
|
panic("log model must implement Reset() method: " + t.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查该方法是否属于当前类型(而不是继承自 BaseLog 且没有被重写)
|
// 检查该方法是否属于当前类型(而不是继承自 BaseLog 且没有被重写)
|
||||||
baseResetMethod, _ := reflect.PointerTo(reflect.TypeOf(BaseLog{})).MethodByName("Reset")
|
baseResetMethod, _ := reflect.PointerTo(reflect.TypeOf(BaseLog{})).MethodByName("Reset")
|
||||||
if method.Func.Pointer() == baseResetMethod.Func.Pointer() {
|
if method.Func.Pointer() == baseResetMethod.Func.Pointer() {
|
||||||
@ -144,6 +147,11 @@ func extractMetaFields(model any) []MetaField {
|
|||||||
if tag != "" {
|
if tag != "" {
|
||||||
parts := strings.Split(tag, ",")
|
parts := strings.Split(tag, ",")
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "attachBefore" {
|
||||||
|
meta.AttachBefore = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
kv := strings.SplitN(part, ":", 2)
|
kv := strings.SplitN(part, ":", 2)
|
||||||
if len(kv) == 2 {
|
if len(kv) == 2 {
|
||||||
key := strings.TrimSpace(kv[0])
|
key := strings.TrimSpace(kv[0])
|
||||||
@ -157,6 +165,12 @@ func extractMetaFields(model any) []MetaField {
|
|||||||
meta.WithoutKey = (val == "true")
|
meta.WithoutKey = (val == "true")
|
||||||
case "hide":
|
case "hide":
|
||||||
meta.Hide = (val == "true")
|
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"`
|
LogName string `log:"pos:0,color:cyan,hide:true"`
|
||||||
LogType string `log:"pos:1,color:magenta,hide:true"`
|
LogType string `log:"pos:1,color:magenta,hide:true"`
|
||||||
LogTime int64 `log:"pos:2,format:time"`
|
LogTime int64 `log:"pos:2,format:time"`
|
||||||
TraceId string `log:"pos:3,color:blue"`
|
TraceId string `log:"pos:3,color:gray,withoutkey:true"`
|
||||||
Image string `log:"pos:4,color:darkGray,hide:true"`
|
Image string `log:"pos:4,hide:true"`
|
||||||
Server string `log:"pos:5,color:darkGray,hide:true"`
|
Server string `log:"pos:5,hide:true"`
|
||||||
Extra map[string]any `log:"pos:1000"`
|
Extra map[string]any `log:"pos:1000"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
131
viewer.go
131
viewer.go
@ -2,7 +2,9 @@ package log
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -12,6 +14,7 @@ import (
|
|||||||
|
|
||||||
var errorLineMatcher = regexp.MustCompile(`(\w+\.go:\d+)`)
|
var errorLineMatcher = regexp.MustCompile(`(\w+\.go:\d+)`)
|
||||||
var codeFileMatcher = regexp.MustCompile(`(\w+?\.)(go|js)`)
|
var codeFileMatcher = regexp.MustCompile(`(\w+?\.)(go|js)`)
|
||||||
|
var workspaceRoot, _ = os.Getwd()
|
||||||
|
|
||||||
func Viewable(line string) string {
|
func Viewable(line string) string {
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
@ -76,17 +79,16 @@ func Viewable(line string) string {
|
|||||||
if m.Name == "Extra" {
|
if m.Name == "Extra" {
|
||||||
extraMap, ok := v.(map[string]any)
|
extraMap, ok := v.(map[string]any)
|
||||||
if ok && len(extraMap) > 0 {
|
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(" ")
|
||||||
builder.WriteString(shell.Style(shell.TextWhite, shell.Dim, shell.Italic, k+":"))
|
builder.WriteString(shell.Style(shell.TextWhite, shell.Dim, shell.Italic, k+":"))
|
||||||
vStr := ""
|
builder.WriteString(renderValue(ev, 0, ""))
|
||||||
switch ev.(type) {
|
|
||||||
case map[string]any, []any:
|
|
||||||
vStr, _ = cast.ToJSON(ev)
|
|
||||||
default:
|
|
||||||
vStr = cast.String(ev)
|
|
||||||
}
|
|
||||||
builder.WriteString(vStr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@ -95,9 +97,18 @@ func Viewable(line string) string {
|
|||||||
if m.Name == "CallStacks" {
|
if m.Name == "CallStacks" {
|
||||||
callStacksList, ok := v.([]any)
|
callStacksList, ok := v.([]any)
|
||||||
if ok && len(callStacksList) > 0 {
|
if ok && len(callStacksList) > 0 {
|
||||||
|
stackColor := shell.TextRed
|
||||||
|
if strings.Contains(strings.ToLower(logType), "warn") {
|
||||||
|
stackColor = shell.TextYellow
|
||||||
|
}
|
||||||
|
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
for _, vi := range callStacksList {
|
for _, vi := range callStacksList {
|
||||||
vStr := cast.String(vi)
|
vStr := cast.String(vi)
|
||||||
|
if workspaceRoot != "" {
|
||||||
|
vStr = strings.TrimPrefix(vStr, workspaceRoot)
|
||||||
|
vStr = strings.TrimPrefix(vStr, "/")
|
||||||
|
}
|
||||||
postfix := ""
|
postfix := ""
|
||||||
if pos := strings.LastIndexByte(vStr, '/'); pos != -1 {
|
if pos := strings.LastIndexByte(vStr, '/'); pos != -1 {
|
||||||
postfix = vStr[pos+1:]
|
postfix = vStr[pos+1:]
|
||||||
@ -108,7 +119,7 @@ func Viewable(line string) string {
|
|||||||
}
|
}
|
||||||
builder.WriteString(" ")
|
builder.WriteString(" ")
|
||||||
builder.WriteString(shell.Style(shell.Dim, vStr))
|
builder.WriteString(shell.Style(shell.Dim, vStr))
|
||||||
builder.WriteString(shell.Style(shell.TextWhite, postfix))
|
builder.WriteString(shell.Style(stackColor, postfix))
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,28 +131,42 @@ func Viewable(line string) string {
|
|||||||
if m.Format == "time" {
|
if m.Format == "time" {
|
||||||
// Convert int64 ns to time string
|
// Convert int64 ns to time string
|
||||||
logTime := time.Unix(0, cast.Int64(v))
|
logTime := time.Unix(0, cast.Int64(v))
|
||||||
vStr = logTime.Format("01-02 15:04:05.000")
|
dateStr := logTime.Format("01-02")
|
||||||
if m.Color == "" {
|
timeStr := logTime.Format("15:04:05")
|
||||||
builder.WriteString(shell.White(shell.Bold, vStr))
|
milliStr := logTime.Format(".000")
|
||||||
|
|
||||||
|
if builder.Len() > 0 {
|
||||||
builder.WriteString(" ")
|
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 {
|
} else {
|
||||||
vStr = cast.String(v)
|
vStr = renderValue(v, m.Precision, m.Color)
|
||||||
if vStr == "" {
|
if vStr == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if builder.Len() > 0 {
|
if builder.Len() > 0 {
|
||||||
builder.WriteString(" ")
|
if m.AttachBefore {
|
||||||
|
builder.WriteString(":")
|
||||||
|
} else {
|
||||||
|
builder.WriteString(" ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.WithoutKey {
|
if !m.WithoutKey && !m.AttachBefore {
|
||||||
builder.WriteString(shell.Style(shell.TextWhite, shell.Dim, shell.Italic, m.Name+":"))
|
name := m.KeyName
|
||||||
|
if name == "" {
|
||||||
|
name = m.Name
|
||||||
|
}
|
||||||
|
builder.WriteString(shell.Style(shell.TextWhite, shell.Dim, shell.Italic, name+":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.WriteString(applyColor(vStr, m.Color))
|
builder.WriteString(vStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.String()
|
return builder.String()
|
||||||
@ -184,6 +209,12 @@ func ToJSON(line string) string {
|
|||||||
if m.Name == "" {
|
if m.Name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name := m.KeyName
|
||||||
|
if name == "" {
|
||||||
|
name = m.Name
|
||||||
|
}
|
||||||
|
|
||||||
if m.Name == "Extra" {
|
if m.Name == "Extra" {
|
||||||
if extraMap, ok := v.(map[string]any); ok {
|
if extraMap, ok := v.(map[string]any); ok {
|
||||||
for k, ev := range extraMap {
|
for k, ev := range extraMap {
|
||||||
@ -191,7 +222,7 @@ func ToJSON(line string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result[m.Name] = v
|
result[name] = v
|
||||||
}
|
}
|
||||||
} else if cast.String(v) != "0" {
|
} else if cast.String(v) != "0" {
|
||||||
result[fmt.Sprintf("Extra%d", i)] = v
|
result[fmt.Sprintf("Extra%d", i)] = v
|
||||||
@ -216,7 +247,7 @@ func applyColor(text string, color string) string {
|
|||||||
return shell.Yellow(text)
|
return shell.Yellow(text)
|
||||||
case "green":
|
case "green":
|
||||||
return shell.Green(text)
|
return shell.Green(text)
|
||||||
case "gray", "darkGray":
|
case "gray":
|
||||||
return shell.Style(shell.Dim, text)
|
return shell.Style(shell.Dim, text)
|
||||||
default:
|
default:
|
||||||
return text
|
return text
|
||||||
@ -233,3 +264,61 @@ func fallbackRenderArray(arr []any) string {
|
|||||||
}
|
}
|
||||||
return builder.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)
|
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