# log **Repository Path**: idcu-go/log ## Basic Information - **Project Name**: log - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-22 - **Last Updated**: 2026-09-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # gitee.com/idcu-go/log > Current version: **v0.2.3** · Module type: **general module · infra** > Machine-readable manifest: [module.yaml](module.yaml) · API signatures: [docs/api.sig](docs/api.sig) · Human contract: [docs/api.md](docs/api.md) rolling file log sink with read + write ## 1. Positioning + Current Version > Module type: **generic public module · infra** (rolling log sink, cross-project reusable). A reusable Go logging module providing both **write** and **read** capabilities, directly importable across projects: - **Write**: size-rolling file logs (`FileLogSink`) + start/end-time naming; supports mirroring to stderr; multi-sink aggregation. - **Read**: enumerate log segments by filename, query across files by time range, read the latest segment / tail N lines, and parse lines into structured entries. ## 2. Installation ```bash go get gitee.com/idcu-go/log@v0.2.3 ``` **Dependencies**: - Internal (gitee.com/idcu-go/*): `timefmt`, `yamlutil` - Purpose: timefmt formats log timestamps; yamlutil handles log config. gopkg.in/yaml.v3 (indirect) comes with it, used to parse YAML in config. - External (third-party): no direct dependencies (yaml.v3 is indirect) To temporarily reference by local path inside a monorepo, add a `replace` in the consuming `go.mod` (development only; remove before release): ```go // go.mod replace gitee.com/idcu-go/log => ../idcu/log ``` ## 3. API Signatures Complete signature list: [docs/api.sig](docs/api.sig) (generated); the table below is generated from source by `meta/tools/apisig` — **do not edit by hand**. | Symbol | Signature | Notes | | --- | --- | --- | | `BuildLogSinks` | `func BuildLogSinks(logs []NamedLog, def LogConfig) ([]LogSink, error)` | BuildLogSinks 把具名日志配置 + 默认参数构造为 LogSink 列表。 | | `Debug` | `func Debug(msg string, args ...any)` | Debug 记录包级 Debug 级别日志。 | | `Error` | `func Error(msg string, args ...any)` | Error 记录包级 Error 级别日志。 | | `Info` | `func Info(msg string, args ...any)` | Info 记录包级 Info 级别日志。 | | `Init` | `func Init(level Level, jsonOutput bool)` | Init 初始化全局日志。 | | `InitWriter` | `func InitWriter(w io.Writer, level Level, jsonOutput bool)` | InitWriter 使用自定义 writer 初始化日志(用于测试)。 | | `L` | `func L() *Logger` | L 返回全局默认日志器(线程安全,未初始化时惰性创建)。 | | `Latest` | `func Latest(dir, prefix string) (*Segment, error)` | Latest 返回 dir 下指定 prefix 最新(Start 最大)的一个日志段; | | `ListSegments` | `func ListSegments(dir, prefix string) ([]Segment, error)` | ListSegments 列出 dir 下所有匹配 "-_.log" 的日志段, 按 Start 升序排序; | | `MultiSink` | `func MultiSink(sinks ...LogSink) *log.Logger` | MultiSink 把多个 sink 合并成一个 *log.Logger(并发写用 io.MultiWriter)。 | | `NewFileLogSink` | `func NewFileLogSink(name, dir, prefix string, maxSize int64, console bool) (*FileLogSink, error)` | NewFileLogSink 创建文件 sink。 | | `ParseLine` | `func ParseLine(line string) (LogEntry, bool)` | ParseLine 解析一行标准 log 默认格式: "2006/01/02 15:04:05 ..."(对应 log.LstdFlags)或 "2006/01/02 15:04:05.999 ..."。 | | `ParseSegmentName` | `func ParseSegmentName(name string) (prefix string, start, end time.Time, ok bool)` | ParseSegmentName 从文件名解析出前缀、起、止时间。 | | `ReadFile` | `func ReadFile(path string, opts *ReadOptions) ([]LogEntry, error)` | ReadFile 读取单个日志文件并结构化解析所有行(不过时间过滤)。 | | `ReadRange` | `func ReadRange(dir, prefix string, from, to time.Time, opts *ReadOptions) ([]LogEntry, error)` | ReadRange 在 [from, to] 时间范围内,跨多个日志段文件检索并解析日志。 | | `Tail` | `func Tail(path string, n int) ([]string, error)` | Tail 读取文件末尾 n 行(不解析)。 | | `Warn` | `func Warn(msg string, args ...any)` | Warn 记录包级 Warn 级别日志。 | | `WithModule` | `func WithModule(module string) *Logger` | WithModule 返回一个带模块名的日志器副本。 | | `FileLogSink.Close` | `func (*FileLogSink) Close() error` | Close 关闭文件,并把当前文件名里的 end 刷新为真正的退出时刻, 使文件名精确反映该日志段的「开始—结束」区间。 | | `FileLogSink.Log` | `func (*FileLogSink) Log() *log.Logger` | Log 返回绑定本 sink 的 *log.Logger。 | | `FileLogSink.Name` | `func (*FileLogSink) Name() string` | Name 返回 sink 名字。 | | `FileLogSink.Write` | `func (*FileLogSink) Write(p []byte) (int, error)` | Write 实现 io.Writer:带锁写入,写满 maxSize 时先滚动再写; | | `Logger.Debug` | `func (*Logger) Debug(msg string, args ...any)` | Debug 记录 Debug 级别日志。 | | `Logger.Error` | `func (*Logger) Error(msg string, args ...any)` | Error 记录 Error 级别日志。 | | `Logger.Info` | `func (*Logger) Info(msg string, args ...any)` | Info 记录 Info 级别日志。 | | `Logger.Warn` | `func (*Logger) Warn(msg string, args ...any)` | Warn 记录 Warn 级别日志。 | | `Logger.With` | `func (*Logger) With(args ...any) *Logger` | With 返回一个带额外字段的日志器副本。 | | `Logger.WithModule` | `func (*Logger) WithModule(module string) *Logger` | WithModule 返回一个带模块名的日志器副本。 | | `FileLogSink` | `type FileLogSink struct { … }` | FileLogSink 文件型日志 sink:按 max_size 滚动,文件名带起止时间。 | | `Level` | `type Level int` | Level 日志级别,与 slog.Level 对齐。 | | `LogConfig` | `type LogConfig struct { Dir MaxSize Console Prefix }` | LogConfig 全局日志默认(logging 段)。 | | `LogEntry` | `type LogEntry struct { Time Level Message Raw }` | LogEntry 是一行日志解析后的结构化表示。 | | `LogSink` | `type LogSink interface { Log Name Close Write }` | LogSink 是所有日志输出目标统一的接口。 | | `Logger` | `type Logger struct { … }` | Logger 封装的结构化日志实例(带可选 module)。 | | `NamedLog` | `type NamedLog struct { Name Type Dir MaxSize Console Prefix }` | NamedLog 是「日志注册表(logs)」里的一个具名 sink 配置。 | | `ReadOptions` | `type ReadOptions struct { KeepUnparsed LevelFilter }` | ReadOptions 控制 ReadRange / ReadFile 的行为。 | | `Segment` | `type Segment struct { Path Start End Size }` | Segment 描述一个日志段文件及其起止时间(取自文件名)。 | | `LevelDebug` | `const LevelDebug Level = Level(slog.LevelDebug)` | LevelDebug 调试级别。 | | `LevelError` | `const LevelError Level = Level(slog.LevelError)` | LevelError 错误级别。 | | `LevelInfo` | `const LevelInfo Level = Level(slog.LevelInfo)` | LevelInfo 信息级别。 | | `LevelWarn` | `const LevelWarn Level = Level(slog.LevelWarn)` | LevelWarn 警告级别。 | The `LogSink` interface: `Log() *log.Logger` / `Name() string` / `Close() error` / `Write(p []byte) (int, error)`. ## 4. Key Types / Configuration ```go // 文件型 sink:按 maxSize 滚动,文件名带起止时间 type FileLogSink struct { name string // sink 名字 dir string // 日志目录 prefix string // 文件名前缀(默认 "app") maxSize int64 // 单文件最大字节数,<=0 表示不限制 start time.Time // 进程启动时刻(文件名第一段,固定) end time.Time // 最近一次滚动/退出时刻(文件名第二段) file *os.File // 当前文件句柄 curPath string // 当前文件绝对路径,用于 Close 时 rename 刷新 end size int64 // 当前文件已写字节数,用于判断是否滚动 console bool // 是否同时镜像到 stderr logger *log.Logger // 绑定本 sink 的 *log.Logger } // 具名 sink 配置(用于 YAML 注册表 logs) type NamedLog struct { Name string // sink 唯一名字,供引用 Type string // 当前仅支持 "file"(默认) Dir string // 日志目录 MaxSize yamlutil.Size // 单文件最大字节数(0=不限制,支持 "10MB" 写法) Console bool // 是否镜像到 stderr(默认 false) Prefix string // 文件名前缀(默认 "app") } // 全局日志默认(logging 段) type LogConfig struct { Dir string // 默认日志目录 MaxSize yamlutil.Size // 默认单文件最大字节数 Console bool // 默认是否镜像控制台 Prefix string // 默认文件名前缀 } // 读侧:一个日志段文件及其起止时间(取自文件名) type Segment struct { Path string // 文件绝对/相对路径 Start time.Time // 开始时间 End time.Time // 结束时间;未结束/未知时为 zero Size int64 // 文件字节数 } // 读侧:一行日志解析后的结构化表示 type LogEntry struct { Time time.Time // 行首时间戳(本地时区),解析失败为 zero Level string // INFO / WARN / ERROR ...(含中文识别),未识别为空 Message string // 时间戳与级别之后的正文 Raw string // 原始行 } // 读侧:ReadRange / ReadFile 的行为控制 type ReadOptions struct { KeepUnparsed bool // 保留无法解析时间戳的行(作为 Raw),默认丢弃 LevelFilter string // 仅保留指定级别(大小写不敏感),空表示不过滤 } ``` > Byte sizes use the `Size` type from `gitee.com/idcu-go/yamlutil` (e.g. `yamlutil.Size(10<<20)`, or `"10MB"`/`"1GB"` in YAML); this package does not ship its own `Size`. ## 5. Minimal Usage Example Distilled from `_test.go`, works out of the box (write a rolling log and read it back): ```go package main import ( "log" "time" "gitee.com/idcu-go/log" "gitee.com/idcu-go/yamlutil" ) func main() { // 1) 写:最大 10MB、同时镜像到 stderr 的滚动文件 sink sinks, err := logutil.BuildLogSinks([]logutil.NamedLog{ {Name: "default", Type: "file", Dir: "logs", MaxSize: yamlutil.Size(10 << 20), Console: true}, }, logutil.LogConfig{}) if err != nil { log.Fatal(err) } log.SetOutput(logutil.MultiSink(sinks...).Writer()) log.Println("hello world") // 写入 logs/-...log,并镜像到 stderr // 2) 读:查询最近 24 小时、逐行结构化解析 entries, err := logutil.ReadRange("logs", "app", time.Now().Add(-24*time.Hour), time.Now(), nil) if err != nil { log.Fatal(err) } for _, e := range entries { _ = e // e.Time / e.Level / e.Message / e.Raw } // 3) 关闭并刷新文件名里的结束时间 for _, s := range sinks { _ = s.Close() } } ``` It can also be driven by a YAML config (fields in the `NamedLog` / `LogConfig` tables above): ```go var cfg struct { Logs []logutil.NamedLog `yaml:"logs"` Logging logutil.LogConfig `yaml:"logging"` } // ... 读取 yaml ... sinks, _ := logutil.BuildLogSinks(cfg.Logs, cfg.Logging) log.SetOutput(logutil.MultiSink(sinks...).Writer()) ``` Log filename format: `-_(_).log`; the first segment is the creation time, the second the last-roll time, and `_N` the same-second repeat-roll sequence number. ## 6. How It Works / Boundary Semantics - **Rolling split**: `Write` writes under lock; when `maxSize>0 && size>0 && size+len(p) > maxSize`, it `rotate`s first, then writes. `maxSize<=0` means unlimited single-file size, never rolling. - **Start/end-time naming and refresh**: `start` is fixed to the process start time; `end` is refreshed to the current time on every `rotate`; `Close` renames the current file to refresh `end` to the real exit time, so the filename precisely reflects the run interval. - **Same-second overwrite protection**: if the target filename is already taken (a file with the same `end` timestamp exists), **both `rotate` and `Close`** append a `_` sequence (e.g. `demo-..._1.log`), guaranteeing unique filenames and no data loss. - **Concurrency safety**: `FileLogSink` guards `Write` / `Close` / `Log` with `sync.Mutex` (the `logger` is rebuilt on rotation, so an unlocked read would race with it). Note that `rotate` may be called while `Write` holds the lock, so the rotate hint writes to the file directly and **never goes through `s.logger`** (otherwise it would recurse into `Write` and re-acquire the lock, deadlocking). - **Size accounting**: after opening a new file, `rotate` also counts the length of the "log file created" hint into `size` (previously it reset to zero and ignored the hint), otherwise every rotation under-counts a line and the `max_size` decision is delayed. - **Structured logging (module.go, α draft)**: both the package-level `logutil.Info/Warn/…` and the method-level `Logger.Info/…` attach a `caller` field automatically; the two paths need different stack depths — with the extra wrapper frame, reusing the same skip makes `caller` always point at this package's wrapper, defeating its purpose. - **Level recognition (read side)**: `ParseLine` recognizes English levels (INFO/WARN/ERROR/DEBUG/FATAL/PANIC etc.) and Chinese ones (警告/错误/失败/致命/调试), normalizing them (WARNING→WARN, ERR→ERROR, FATAL/PANIC→ERROR). `ReadOptions.LevelFilter` filters case-insensitively; `KeepUnparsed` controls whether lines with unparseable timestamps are kept. - **Timezone consistency**: filenames and line parsing both use `time.ParseInLocation(time.Local)`, aligned with `time.Now()` (local time), avoiding UTC/local misalignment causing `ReadRange` misses. - **Default fallbacks (`BuildLogSinks`)**: empty `type` defaults to `"file"`; empty `dir` falls back to `def.Dir` then `"logs"`; empty `prefix` falls back to `def.Prefix`; in `NewFileLogSink`, empty `dir` defaults to `"."` and empty `prefix` to `"app"`. A duplicate name only takes effect on first use with a warning; unsupported types are warned and ignored. - **MultiSink edge cases**: empty `sinks` falls back to writing stderr; a single `sink` returns its `Log()` directly. ## 7. When to Use / When Not to Use **Use it when**: - Terminal / service programs that need size-rolling disk logs to avoid a single log file growing without limit. - Multi-target log output needing both disk persistence and console (stderr) mirroring. - Offline log search / replay panels: query across files by filename or time range, read the latest segment or tail N lines. - Structurally parsing log lines (including Chinese level recognition) for log analysis and alert extraction. - Unified registration across projects: reuse via `NamedLog` + `LogConfig` (YAML) without reimplementing. **Do not use it when**: - You need centralized / remote log shipping (syslog / HTTP / Loki, etc.): this module implements only a local file sink, and `NamedLog.Type` currently supports only `"file"`. - Your log filenames do not follow `-_.log`, or the leading timestamp does not match `2006/01/02 15:04:05[.999]`: the read side (`ListSegments`/`ReadRange`/`ParseLine`) will fail to enumerate segments or parse lines (`ParseSegmentName`/`ParseLine` return `ok=false`). - You need multiple processes / replicas to safely write the same log file, or you need day-based archiving and automatic cleanup of old logs: `FileLogSink`'s concurrency protection is single-process only, and it only rolls by `maxSize` without retention/cleanup. ## 8. Changelog Version history is sourced solely from repository git tags (`git tag -l`); see CHANGELOG.md for details. This file does not maintain a handwritten version list.