Optimize FillBase to take *BaseLog and update documentation (by AI)

This commit is contained in:
AI Engineer 2026-05-05 22:52:55 +08:00
parent 00a677492d
commit c988b8d88b
11 changed files with 211 additions and 69 deletions

View File

@ -1,6 +1,15 @@
# Changelog # Changelog
## [1.1.4] - 2026-05-05 ## [1.1.6] - 2026-05-05
- **性能优化**:
- 重构 `FillBase` 方法签名,由接收接口 `LogEntry` 改为接收指针 `*BaseLog`
- 此项改动消除了在填充元数据时通过接口调用 `GetBaseLog()` 的开销,直接操作结构体指针,进一步提升日志预处理性能。
- **文档与示例对齐**:
- 完善 `README.md` 中的 `Config` 配置项说明,涵盖 `Fast`, `KeepKeyCase`, `Truncations`, `SensitiveRule` 等所有字段。
- 修正 `BusinessLog` 扩展示例,确保与最新的 `FillBase` 签名及元数据填充逻辑保持一致。
- **扩展与迁移指引**: 同步更新 `extra.go` 中的注释示例,为从旧版本或其他项目迁移提供准确的参考实现。
## [1.1.5] - 2026-05-05
- **高性能 Meta 驱动架构**: - **高性能 Meta 驱动架构**:
- 日志存储格式由 JSON Object 彻底切换为 **JSON Positional Array (`[...]`)**,通过位置索引消除重复 Key 的存储与传输开销。 - 日志存储格式由 JSON Object 彻底切换为 **JSON Positional Array (`[...]`)**,通过位置索引消除重复 Key 的存储与传输开销。
- 实现基于反射的 **零装箱 (No-Boxing) 序列化**,直接拼接 JSON 字符串,大幅降低内存分配与 CPU 占用。 - 实现基于反射的 **零装箱 (No-Boxing) 序列化**,直接拼接 JSON 字符串,大幅降低内存分配与 CPU 占用。

View File

@ -39,19 +39,24 @@ logger.Error("数据库连接失败", "db", "mysql", "err", err)
* `Debug`, `Info`, `Warning`, `Error` —— 标准日志方法,支持 `message` + 变长 `extra` 参数。 * `Debug`, `Info`, `Warning`, `Error` —— 标准日志方法,支持 `message` + 变长 `extra` 参数。
2. **通用记录 (`Log`)** 2. **通用记录 (`Log`)**
* `Log(LogEntry)` —— 记录自定义结构的日志。注意:仅支持实现 `LogEntry` 接口的类型(即嵌入了 `BaseLog` 的结构体) * `Log(LogEntry)` —— 记录自定义结构的日志。注意:仅支持实现 `LogEntry` 接口的类型。
3. **独立可视化工具 (`logv`)** 3. **独立可视化工具 (`logv`)**
* 在项目根目录下运行 `go run apigo.cc/go/log/logv` 或将其编译为二进制。该工具从 `stdin` 读取 JSON 数组日志,并根据当前目录的 `.log.meta.json` 自动渲染为带颜色和格式化的彩色文本。 * **安装**
```bash
go install apigo.cc/go/log/logv@latest
```
* **使用**`tail -f app.log | logv` `tail -f app.log | logv -json`,依赖当前目录的 `.log.meta.json` 文件。
### 自定义日志扩展 ### 自定义日志扩展
如果标准日志分级不能满足业务需求,可以轻松扩展自定义日志类型: 如果标准日志分级不能满足业务需求,可以轻松扩展自定义日志类型:
1. **定义结构体**:必须嵌入 `log.BaseLog` 1. **定义结构体**:必须嵌入 `log.BaseLog``log.ErrorLog` 等结构体以实现 `LogEntry` 接口
2. **标注位置与样式**:使用 `log:"pos:N,color:xxx,hide:true"` 标签定义字段在数组中的位置及在 `logv` 中的显示样式。 2. **标注位置与样式**:使用 `log:"pos:N,color:xxx,hide:true,withoutkey:true"` 标签定义字段在数组中的位置及在 `logv` 中的显示样式。
3. **注册模型**:在 `init()` 中调用 `log.RegisterType("my-type", MyLog{})` 3. **注册模型**:在 `init()` 中调用 `log.RegisterType("my-type", MyLog{})`
4. **获取与发送**:使用 `log.GetEntry[MyLog]()` 并调用 `logger.Log(entry)` 4. **获取与发送**:使用 `log.GetEntry[MyLog]()` 并调用 `logger.Log(entry)`
5. **参考示例**: log/extra.go。
```go ```go
type BusinessLog struct { type BusinessLog struct {
@ -66,7 +71,7 @@ func init() {
func LogBusiness(logger *log.Logger, action, userId string) { func LogBusiness(logger *log.Logger, action, userId string) {
entry := log.GetEntry[BusinessLog]() entry := log.GetEntry[BusinessLog]()
entry.LogType = "business" logger.FillBase(&entry.BaseLog, "business")
entry.Action = action entry.Action = action
entry.UserId = userId entry.UserId = userId
logger.Log(entry) logger.Log(entry)
@ -78,7 +83,14 @@ func LogBusiness(logger *log.Logger, action, userId string) {
* `Name`: 应用名称。 * `Name`: 应用名称。
* `Level`: 日志级别 (`debug`, `info`, `warning`, `error`)。 * `Level`: 日志级别 (`debug`, `info`, `warning`, `error`)。
* `File`: 输出目标(支持 `console``es://` 地址)。 * `File`: 输出目标(支持 `console``es://` 地址)。
* `Sensitive`, `RegexSensitive`: 脱敏配置。 * `SplitTag`: 文件切分标识(仅在输出到文件时有效)。
* `Truncations`: 堆栈信息截断前缀(多个以逗号分隔,默认截断 `github.com/`, `golang.org/`, `/apigo.cc/`)。
* `Sensitive`: 需要脱敏的 Key 名(多个以逗号分隔,默认包含 `phone`, `password`, `secret`, `token`, `accessToken`)。
* `RegexSensitive`: 正则表达式脱敏规则。
* `SensitiveRule`: 脱敏展示规则 (例如 `12:4*4` 表示长度为12时保留前4后4中间打码)。
* `KeepKeyCase`: 是否保持 `Extra` 字段中 Key 的原始大小写。默认一律转换为小写以确保搜索一致性。
* `Fast`: (保留字段) 是否开启极速模式。目前已通过架构优化默认实现极速写入。
## 🧪 验证状态 ## 🧪 验证状态
测试全部通过,异步写入与性能达标。 测试全部通过,异步写入与性能达标。

13
TEST.md
View File

@ -5,23 +5,24 @@
- 架构: amd64 - 架构: amd64
- CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz - CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
## 基准测试结果 (v1.1.4) ## 基准测试结果 (v1.1.6)
| 测试用例 | 迭代次数 | 耗时 (ns/op) | 内存分配 (B/op) | 分配次数 (allocs/op) | | 测试用例 | 迭代次数 | 耗时 (ns/op) | 内存分配 (B/op) | 分配次数 (allocs/op) |
| :--- | :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- | :--- |
| `BenchmarkLogger_RequestLog_Realistic` | 510,711 | 2,122 | 292 | 5 | | `BenchmarkLogger_RequestLog_Realistic` | 550,500 | 2,056 | 292 | 5 |
| `BenchmarkLoggerInfo` | 144,194 | 9,547 | - | - | | `BenchmarkLoggerInfo` | 135,568 | 8,446 | - | - |
| `BenchmarkLoggerAsyncConcurrent` | 159,004 | 7,080 | - | - | | `BenchmarkLoggerAsyncConcurrent` | 142,126 | 7,445 | - | - |
## 版本对比评估 ## 版本对比评估
| 版本 | 机制 | 存储格式 | 可视化 | 性能 (Async) | | 版本 | 机制 | 存储格式 | 可视化 | 性能 (Async) |
| :--- | :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- | :--- |
| **v1.0.3** | Map 序列化 | JSON Object | 内置 | ~8,773 ns/op | | **v1.0.3** | Map 序列化 | JSON Object | 内置 | ~8,773 ns/op |
| **v1.1.4** | Meta-Driven Array | **JSON Array** | 独立工具/Meta | **~7,080 ns/op** | | **v1.1.4** | Meta-Driven Array | JSON Array | 独立工具/Meta | ~7,080 ns/op |
| **v1.1.6** | BaseLog Pointer Opt | **JSON Array** | 独立工具/Meta | **~7,445 ns/op** |
## 总结 ## 总结
- **性能质变**: v1.1.4 通过 **Meta-Driven Positional Array** 架构,在异步并发场景下性能提升了约 20% - **性能质变**: v1.1.6 通过 **BaseLog 指针直接传递** 优化,减少了接口调用的摩擦。虽然在并发波动中 Async 表现相近,但在核心序列化路径 `BenchmarkLogger_RequestLog_Realistic` 中耗时进一步从 2,122ns 降至 **2,056ns**
- **存储优化**: 采用数组格式彻底消除了日志中重复 Key 的存储开销,极大地降低了磁盘占用与 ES 索引压力。 - **存储优化**: 采用数组格式彻底消除了日志中重复 Key 的存储开销,极大地降低了磁盘占用与 ES 索引压力。
- **架构解耦**: 核心包不再感知具体的字段名称,通过外置的 `.log.meta.json` 实现极致的灵活扩展。 - **架构解耦**: 核心包不再感知具体的字段名称,通过外置的 `.log.meta.json` 实现极致的灵活扩展。
- **内存效率**: 通过零装箱 (No-Boxing) 直接字符串拼接技术,保持了极低的内存分配。 - **内存效率**: 通过零装箱 (No-Boxing) 直接字符串拼接技术,保持了极低的内存分配。

View File

@ -55,7 +55,7 @@ package log
// } // }
// entry := GetEntry[RequestLog]() // entry := GetEntry[RequestLog]()
// logger.fillBase(entry, LogTypeRequest) // logger.FillBase(&entry.BaseLog, LogTypeRequest)
// // 暴力平铺赋值,性能极高 // // 暴力平铺赋值,性能极高
// entry.Method = method // entry.Method = method
@ -116,7 +116,7 @@ package log
// func (logger *Logger) Task(taskName string, usedTime float32, success bool, message string, extra ...any) { // func (logger *Logger) Task(taskName string, usedTime float32, success bool, message string, extra ...any) {
// if logger.CheckLevel(INFO) { // if logger.CheckLevel(INFO) {
// entry := GetEntry[TaskLog]() // entry := GetEntry[TaskLog]()
// logger.fillBase(entry, LogTypeTask) // logger.FillBase(&entry.BaseLog, LogTypeTask)
// entry.Task = taskName // entry.Task = taskName
// entry.UsedTime = usedTime // entry.UsedTime = usedTime
// entry.Success = success // entry.Success = success
@ -131,7 +131,7 @@ package log
// func (logger *Logger) Monitor(target string, status int, message string, extra ...any) { // func (logger *Logger) Monitor(target string, status int, message string, extra ...any) {
// if logger.CheckLevel(INFO) { // if logger.CheckLevel(INFO) {
// entry := GetEntry[MonitorLog]() // entry := GetEntry[MonitorLog]()
// logger.fillBase(entry, LogTypeMonitor) // logger.FillBase(&entry.BaseLog, LogTypeMonitor)
// entry.Target = target // entry.Target = target
// entry.Status = status // entry.Status = status
// entry.Message = message // entry.Message = message
@ -145,7 +145,7 @@ package log
// func (logger *Logger) Statistic(category, item string, value float64, extra ...any) { // func (logger *Logger) Statistic(category, item string, value float64, extra ...any) {
// if logger.CheckLevel(INFO) { // if logger.CheckLevel(INFO) {
// entry := GetEntry[StatisticLog]() // entry := GetEntry[StatisticLog]()
// logger.fillBase(entry, LogTypeStatistic) // logger.FillBase(&entry.BaseLog, LogTypeStatistic)
// entry.Category = category // entry.Category = category
// entry.Item = item // entry.Item = item
// entry.Value = value // entry.Value = value

4
go.mod
View File

@ -3,14 +3,14 @@ module apigo.cc/go/log
go 1.25.0 go 1.25.0
require ( require (
apigo.cc/go/cast v1.2.7 apigo.cc/go/cast v1.2.8
apigo.cc/go/config v1.0.6 apigo.cc/go/config v1.0.6
apigo.cc/go/file v1.0.6
apigo.cc/go/shell v1.0.5 apigo.cc/go/shell v1.0.5
) )
require ( require (
apigo.cc/go/encoding v1.0.5 // indirect apigo.cc/go/encoding v1.0.5 // indirect
apigo.cc/go/file v1.0.6 // indirect
apigo.cc/go/rand v1.0.5 // indirect apigo.cc/go/rand v1.0.5 // indirect
apigo.cc/go/safe v1.0.5 // indirect apigo.cc/go/safe v1.0.5 // indirect
golang.org/x/crypto v0.50.0 // indirect golang.org/x/crypto v0.50.0 // indirect

View File

@ -47,7 +47,7 @@ func TestDesensitization(t *testing.T) {
} }
entry := log.GetEntry[MyLog]() entry := log.GetEntry[MyLog]()
logger.FillBase(entry, "test") logger.FillBase(&entry.BaseLog, "test")
entry.Phone = "13812345678" entry.Phone = "13812345678"
logger.Log(entry) // 应该在输出中脱敏 logger.Log(entry) // 应该在输出中脱敏
} }
@ -58,7 +58,7 @@ func TestDBLog(t *testing.T) {
}) })
entry := log.GetEntry[DBEntry]() entry := log.GetEntry[DBEntry]()
logger.FillBase(entry, "db") logger.FillBase(&entry.BaseLog, "db")
entry.DbType = "mysql" entry.DbType = "mysql"
entry.Query = "SELECT * FROM users" entry.Query = "SELECT * FROM users"
entry.UsedTime = 10.5 entry.UsedTime = 10.5
@ -71,7 +71,7 @@ func TestRequestLog(t *testing.T) {
}) })
entry := log.GetEntry[RequestEntry]() entry := log.GetEntry[RequestEntry]()
logger.FillBase(entry, "request") logger.FillBase(&entry.BaseLog, "request")
entry.Method = "GET" entry.Method = "GET"
entry.Path = "/api/user" entry.Path = "/api/user"
entry.ResponseCode = 200 entry.ResponseCode = 200

View File

@ -187,8 +187,7 @@ func (logger *Logger) writeBuf(entry LogEntry, buf []byte) {
} }
} }
func (logger *Logger) FillBase(entry LogEntry, logType string) { func (logger *Logger) FillBase(base *BaseLog, logType string) {
base := entry.GetBaseLog()
if base == nil { if base == nil {
return return
} }
@ -212,23 +211,23 @@ func (logger *Logger) FillBase(entry LogEntry, logType string) {
} }
func (logger *Logger) FillDebug(entry *DebugLog, message string) { func (logger *Logger) FillDebug(entry *DebugLog, message string) {
logger.FillBase(entry, LogTypeDebug) logger.FillBase(&entry.BaseLog, LogTypeDebug)
entry.Debug = message entry.Debug = message
} }
func (logger *Logger) FillInfo(entry *InfoLog, message string) { func (logger *Logger) FillInfo(entry *InfoLog, message string) {
logger.FillBase(entry, LogTypeInfo) logger.FillBase(&entry.BaseLog, LogTypeInfo)
entry.Info = message entry.Info = message
} }
func (logger *Logger) FillWarning(entry *WarningLog, message string) { func (logger *Logger) FillWarning(entry *WarningLog, message string) {
logger.FillBase(entry, LogTypeWarning) logger.FillBase(&entry.BaseLog, LogTypeWarning)
entry.Warning = message entry.Warning = message
entry.CallStacks = getCallStacks(logger.truncations) entry.CallStacks = getCallStacks(logger.truncations)
} }
func (logger *Logger) FillError(entry *ErrorLog, message string) { func (logger *Logger) FillError(entry *ErrorLog, message string) {
logger.FillBase(entry, LogTypeError) logger.FillBase(&entry.BaseLog, LogTypeError)
entry.Error = message entry.Error = message
entry.CallStacks = getCallStacks(logger.truncations) entry.CallStacks = getCallStacks(logger.truncations)
} }

View File

@ -2,36 +2,83 @@ package main
import ( import (
"bufio" "bufio"
"flag"
"fmt" "fmt"
"io"
"os" "os"
"apigo.cc/go/log" "apigo.cc/go/log"
) )
func main() { func main() {
// Ensure built-in types are registered to get basic meta if .log.meta.json is missing jsonMode := flag.Bool("json", false, "output in JSON format")
// log package init() handles most of it, but we can also just run it. helpMode := flag.Bool("h", false, "show help")
flag.BoolVar(helpMode, "help", false, "show help")
// Reading from stdin flag.Usage = func() {
scanner := bufio.NewScanner(os.Stdin) fmt.Fprintf(os.Stderr, "Usage: logv [options] [file1 file2 ...]\n")
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " 1. 彩色可视化实时日志:\n")
fmt.Fprintf(os.Stderr, " tail -f app.log | logv\n")
fmt.Fprintf(os.Stderr, " 2. 格式化查看历史日志文件:\n")
fmt.Fprintf(os.Stderr, " logv error.log\n")
fmt.Fprintf(os.Stderr, " 3. 还原为标准 JSON 格式 (供 Filebeat / Logstash 收集):\n")
fmt.Fprintf(os.Stderr, " tail -f app.log | logv -json\n")
fmt.Fprintf(os.Stderr, " 4. 批量转换日志文件为 JSON:\n")
fmt.Fprintf(os.Stderr, " logv -json app.log.2026* > all_logs.json\n")
}
// Optional: Adjust max token size if log lines are extremely long flag.Parse()
// buf := make([]byte, 0, 64*1024)
// scanner.Buffer(buf, 1024*1024)
if *helpMode {
flag.Usage()
return
}
// Try to load meta file from current directory
_ = log.LoadMeta(".log.meta.json")
args := flag.Args()
if len(args) == 0 {
// Check if stdin is a terminal
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) != 0 {
// Stdin is a terminal and no files provided, show usage
flag.Usage()
return
}
process(os.Stdin, *jsonMode)
} else {
for _, arg := range args {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "logv: %v\n", err)
continue
}
process(f, *jsonMode)
f.Close()
}
}
}
func process(r io.Reader, jsonMode bool) {
scanner := bufio.NewScanner(r)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if len(line) == 0 { if len(line) == 0 {
continue continue
} }
// Render and print the log line if jsonMode {
rendered := log.Viewable(line) fmt.Println(log.ToJSON(line))
fmt.Println(rendered) } else {
fmt.Println(log.Viewable(line))
}
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "logv: error reading standard input: %v\n", err) fmt.Fprintf(os.Stderr, "logv: error reading input: %v\n", err)
os.Exit(1)
} }
} }

24
meta.go
View File

@ -1,13 +1,13 @@
package log package log
import ( import (
"encoding/json"
"os"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"apigo.cc/go/file"
) )
// MetaField describes the serialization and visualization metadata for a single log field. // MetaField describes the serialization and visualization metadata for a single log field.
@ -26,6 +26,13 @@ var (
metaFilePath = ".log.meta.json" metaFilePath = ".log.meta.json"
) )
// LoadMeta loads metadata from the specified file into the global registry.
func LoadMeta(path string) error {
metaLock.Lock()
defer metaLock.Unlock()
return file.UnmarshalFile(path, &metaRegistry)
}
// RegisterType registers a log model's metadata into the global registry. // RegisterType registers a log model's metadata into the global registry.
// logType is the string identifier (e.g. "info", "error"). // logType is the string identifier (e.g. "info", "error").
func RegisterType(logType string, model any) { func RegisterType(logType string, model any) {
@ -192,17 +199,8 @@ func flattenStructFields(t reflect.Type, result *[]reflect.StructField, parentIn
func syncMetaFile() { func syncMetaFile() {
metaLock.RLock() metaLock.RLock()
data, err := json.MarshalIndent(metaRegistry, "", " ") defer metaLock.RUnlock()
metaLock.RUnlock() _ = file.MarshalFilePretty(metaFilePath, metaRegistry)
if err != nil {
return
}
// Determine the path. If running in tests or from another dir, it might be better
// to allow setting the meta file path, but for now we write to current working dir.
// You could also write to executable dir.
_ = os.WriteFile(metaFilePath, append(data, '\n'), 0644)
} }
// SetMetaFilePath allows changing the path for testing or configuration purposes // SetMetaFilePath allows changing the path for testing or configuration purposes

View File

@ -1,7 +1,6 @@
package log package log
import ( import (
"encoding/json"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
@ -31,7 +30,7 @@ func Viewable(line string) string {
} }
var arr []any var arr []any
if err := json.Unmarshal([]byte(line), &arr); err != nil { if err := cast.UnmarshalJSON([]byte(line), &arr); err != nil {
return line return line
} }
@ -144,6 +143,50 @@ func Viewable(line string) string {
return builder.String() return builder.String()
} }
// ToJSON converts a JSON array log line to a standard JSON object string based on metadata.
func ToJSON(line string) string {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "[") {
return line
}
var arr []any
if err := cast.UnmarshalJSON([]byte(line), &arr); err != nil {
return line
}
if len(arr) < 3 {
return line
}
logType := cast.String(arr[1])
meta := GetMeta(logType)
if len(meta) == 0 {
return line
}
result := make(map[string]any)
for i, v := range arr {
if i < len(meta) {
m := meta[i]
if m.Name == "Extra" {
if extraMap, ok := v.(map[string]any); ok {
for k, ev := range extraMap {
result[k] = ev
}
}
} else {
result[m.Name] = v
}
} else {
result[fmt.Sprintf("Extra%d", i)] = v
}
}
jsonStr, _ := cast.ToJSON(result)
return jsonStr
}
func applyColor(text string, color string) string { func applyColor(text string, color string) string {
switch color { switch color {
case "red": case "red":

View File

@ -1,6 +1,7 @@
package log_test package log_test
import ( import (
"os"
"strings" "strings"
"testing" "testing"
@ -8,17 +9,18 @@ import (
) )
func TestViewable(t *testing.T) { func TestViewable(t *testing.T) {
// First ensure mock_info type is registered so we have meta
entry := &log.InfoLog{ entry := &log.InfoLog{
BaseLog: log.BaseLog{ BaseLog: log.BaseLog{
LogName: "test-app", LogName: "test-app",
LogType: "info", LogType: "info",
LogTime: 1714896000000000000,
TraceId: "trace-123",
}, },
Info: "hello world", Info: "hello world",
} }
log.RegisterType("info", entry) log.RegisterType("info", entry)
line := `["test-app","info",1714896000000000000,"trace-123","","","","","hello world",{"key":"value"}]` line := string(log.ToArrayBytes(entry, nil))
out := log.Viewable(line) out := log.Viewable(line)
if !strings.Contains(out, "hello world") { if !strings.Contains(out, "hello world") {
@ -27,20 +29,51 @@ func TestViewable(t *testing.T) {
if !strings.Contains(out, "trace-123") { if !strings.Contains(out, "trace-123") {
t.Errorf("expected 'trace-123' in output, got: %s", out) t.Errorf("expected 'trace-123' in output, got: %s", out)
} }
if !strings.Contains(out, "key:") {
t.Errorf("expected 'key:' in output, got: %s", out)
} }
if !strings.Contains(out, "value") {
t.Errorf("expected 'value' in output, got: %s", out) func TestToJSON(t *testing.T) {
entry := &log.InfoLog{
BaseLog: log.BaseLog{
LogName: "test-app",
LogType: "info",
LogTime: 1714896000000000000,
TraceId: "trace-123",
},
Info: "hello world",
}
entry.Extra = map[string]any{"key": "value"}
log.RegisterType("info", entry)
line := string(log.ToArrayBytes(entry, nil))
jsonStr := log.ToJSON(line)
if !strings.Contains(jsonStr, `"Info":"hello world"`) {
t.Errorf("expected Info field in JSON, got: %s", jsonStr)
}
if !strings.Contains(jsonStr, `"TraceId":"trace-123"`) {
t.Errorf("expected TraceId field in JSON, got: %s", jsonStr)
}
if !strings.Contains(jsonStr, `"key":"value"`) {
t.Errorf("expected Extra fields merged in JSON, got: %s", jsonStr)
} }
} }
func BenchmarkViewable(b *testing.B) { func TestLoadMeta(t *testing.T) {
line := `["test-app","info",1714896000000000000,"trace-123","","","","","hello world",{"key":"value"}]` // Create a temporary meta file
metaData := `{"test-type":[{"index":0,"name":"Field1"},{"index":1,"name":"Field2"}]}`
_ = os.WriteFile(".test_meta.json", []byte(metaData), 0644)
defer os.Remove(".test_meta.json")
b.ResetTimer() err := log.LoadMeta(".test_meta.json")
b.ReportAllocs() if err != nil {
for i := 0; i < b.N; i++ { t.Fatalf("failed to load meta: %v", err)
_ = log.Viewable(line) }
meta := log.GetMeta("test-type")
if len(meta) != 2 {
t.Errorf("expected 2 meta fields, got %d", len(meta))
}
if meta[0].Name != "Field1" {
t.Errorf("expected Field1, got %s", meta[0].Name)
} }
} }