├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── FUTURE.md
├── HISTORY.md
├── LICENSE
├── Makefile
├── README.en.md
├── README.md
├── _examples
├── basic.go
├── config.go
├── config.json
├── context.go
├── default.go
├── file.go
├── handler.go
├── logger.go
├── option.go
├── performance_test.go
└── writer.go
├── _icons
├── coverage.svg
├── godoc.svg
└── license.svg
├── config.go
├── config_test.go
├── context.go
├── context_test.go
├── default.go
├── default_test.go
├── defaults
└── defaults.go
├── extension
├── config
│ ├── config.go
│ ├── config_test.go
│ ├── parse.go
│ └── parse_test.go
└── fastclock
│ ├── fast_clock.go
│ └── fast_clock_test.go
├── go.mod
├── handler
├── buffer.go
├── buffer_test.go
├── escape.go
├── escape_test.go
├── handler.go
├── handler_test.go
├── tape.go
└── tape_test.go
├── logger.go
├── logger_test.go
├── option.go
├── option_test.go
├── rotate
├── backup.go
├── backup_test.go
├── config.go
├── config_test.go
├── file.go
├── file_test.go
├── option.go
└── option_test.go
└── writer
├── batch.go
├── batch_test.go
├── buffer.go
├── buffer_test.go
├── writer.go
└── writer_test.go
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test Project
2 |
3 | on:
4 | push:
5 | branches:
6 | workflow_dispatch:
7 |
8 | jobs:
9 | test-project:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - run: uname -a
13 | - run: lsb_release -a
14 |
15 | - name: Setup
16 | uses: actions/setup-go@v4
17 | with:
18 | go-version: "1.21"
19 |
20 | - run: go version
21 |
22 | - name: Checkout
23 | uses: actions/checkout@v4
24 |
25 | - name: Test
26 | run: make test
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 | .DS_Store
8 |
9 | # IDE
10 | .idea/
11 | .vscode/
12 | *.iml
13 |
14 | # Program
15 | target/
16 | *.test
17 | *.out
18 | *.log
19 | *.prof
20 |
--------------------------------------------------------------------------------
/FUTURE.md:
--------------------------------------------------------------------------------
1 | ## ✒ 未来版本的新特性 (Features in future versions)
2 |
3 | ### v1.8.x
4 |
5 | * [x] 提高单元测试覆盖率到 80%
6 | * [x] 增加快速时钟,可以非常快速地查询时间
7 | * [ ] 提高单元测试覆盖率到 90%
8 | * [ ] 增加按天日期进行分裂的文件写出器
9 |
10 | ### v1.5.x
11 |
12 | * [x] 考虑结合 Go1.21 新加的 slog 包,做一些生产化的适配和功能
13 | * [x] 增加 context 适配功能
14 | * [x] 增加 Production 现成配置,搭配 Option 方便使用
15 | * [x] 完善 Logger 相关的单元测试
16 | * [x] 调整 writer 包代码,完善单元测试
17 | * [x] 调整 rotate 包代码,完善单元测试
18 | * [x] 完善 config 包功能和单元测试
19 | * [x] 完善性能测试
20 | * [x] 完善示例代码
21 | * [x] 增加属性解析器适配功能
22 | * [x] 优化 handler 和 writer 设计
23 | * [x] 增加一个高可读性且高性能的 handler 实现
24 | * [x] TapeHandler 转义处理
25 | * [x] 提高单元测试覆盖率到 70%
26 |
27 | ### v1.2.x
28 |
29 | * [ ] ~~考虑增加类似于 scope 的机制,可以在 logit 中装载超过一个 global 的 logger~~
30 | * [ ] ~~考虑增加 Production 之类不同级别的现成配置 options,方便使用~~
31 |
32 | ### v1.1.x
33 |
34 | * [x] 调整 global logger 的设计,去除 caller + 1 的代码
35 |
36 | ### v1.0.0
37 |
38 | * [x] 稳定的 API
39 |
40 | ### v0.5.x
41 |
42 | * [x] ~~去掉讨厌的 End() 方法,考虑增加一个 Log 方法,然后直接以 logger.Log(log.Debug("test").Int64("userID", 123)) 这样的形式改造~~
43 |
44 | > 尝试过这种 API 形式,但是实现起来有些别扭,使用起来也很奇怪,虽然没有出现 End 方法,但为了避免 End 方法引入了别的方法,写起来反而繁琐。。
45 |
46 | * [x] 代码优化和重构,包括性能和逻辑上的优化,代码质量上的重构
47 | * [x] 支持增加调用堆栈或者函数名的信息到日志
48 | * [x] 增加批量写入的 Writer,区别于缓冲写入的 Writer,这个批量是按照数量进行缓冲的
49 | * [x] 考虑到标准库的时间格式化耗费的时间和内存都很多,可能会有这方面优化的需求和场景,抽离出获取时间的函数
50 | * [x] ~~缓冲写出器增加分段锁机制增加并发性能,考虑使用 chan 重构设计,可以参考下 zap 的异步处理~~
51 |
52 | > 经过 zap 源码的研究,发现和现有 buffer writer 的逻辑区别不大,而之前使用 chan 尝试过,性能并没有很好,IO 次数还是一样多,容易出现堆积情况。
53 |
54 | * [x] 增加错误监控回调函数
55 | * [x] 增加配置项解析功能,提供配置文件支持
56 | * [x] 加入日志存活天数的特性
57 | * [x] 加入日志存活个数的特性
58 | * [x] 按时间、文件大小自动切割日志文件,并支持过期清理机制
59 |
60 | ### v0.4.x
61 |
62 | * [x] 尝试和 Context 机制结合,传递信息
63 | * [x] 重新支持配置文件,考虑抽离配置文件模块为独立仓库,甚至是设计一种新的配置文件
64 | * [x] depth 放开,可以支持用户包裹
65 | * [x] Log 增加 Caller 方法,方便某些场景下获取调用信息
66 | * [x] 兼容 log 包部分方法,比如 Printf
67 | * [x] 考虑下 interceptor 的实现,在 Log 中加入 context,然后 End 时调用 interceptor 即可
68 |
69 | ### v0.3.x
70 |
71 | 经过一段时间的实际使用,发现了一些不太方便的地方需要进行调整。
72 |
73 | * [x] Handler 的设计太过于抽象,导致很多日志库本身的功能实现过于剥离、插件化
74 | * [x] 类 Json 的配置文件容易嵌套过多,不方便看(这也是 Handler 抽象程度太高导致的)
75 | * [x] 部分 API 使用不太方便,特别是和 Handler 相关的一些功能
76 | * [x] 有些 API 的使用频率确实很低,原本已经屏蔽了一些,但目前的 API 列表还不够清爽
77 | * [x] duration 和 size 的设计导致没办法同时使用,而且加新特性会越来越臃肿
78 | * [ ] 加入日志存活天数的特性
79 | * [ ] 加入日志存活个数的特性
80 | * [ ] ~~使用多个变量替代 map,避免哈希消耗性能~~
81 |
82 | > 这么做还是无法避免一层映射,如果真的要避免映射,就得对 log 方法进行比较大幅度的改造。
83 |
84 | > 总结:原本让我引以为傲的 Handler 在长期的使用下来发现很蛋疼,
85 | > 优点是有,但麻烦也不少,所以需要改造!
86 |
87 | ### v0.0.x - v0.2.x
88 |
89 | * [x] 实现基础的日志功能
90 | * [ ] ~~引入 RollinHook 组件~~
91 |
92 | > 取消这个特性是因为,它的代码入侵太严重了,并且会使代码设计变得很复杂,使用体验也会变差。
93 | > 为了这样一个扩展特性要去改动核心特性的代码,有些喧宾夺主了,所以最后取消了这个组件。
94 |
95 | * [ ] ~~修复配置文件中出现转义字符导致解析出错的问题~~
96 |
97 | > 取消这个特性是因为,配置文件是用户写的,如果存在转义字符的问题,用户自行做转义会更适合一点。
98 |
99 | * [ ] ~~增加 timeout_handler.go,里面是带超时功能的日志处理器包装器~~
100 |
101 | > 取消这个特性是因为,一般在需要获取某个执行时间很长甚至可能一直阻塞的操作的结果时才需要超时,
102 | > 对于日志输出而言,我们并不需要获取日志输出操作的结果,所以这个特性意义不大。
103 | > 还有一个原因就是,实现超时需要使用并发,在超时的任务里终止某个任务,
104 | > 而 Go 语言并没有提供可以停止并销毁一个 goroutine 的方法,所以即使超时了,也没有办法终止这个任务
105 | > 甚至可能造成 goroutine 的阻塞。综合上述,取消这个超时功能的日志处理器包装器。
106 |
107 | * [ ] ~~结合上面几点,以 “并发、缓冲” 为特点进行设计,技术使用 writer 接口进行包装~~
108 |
109 | > 取消这个特性是因为,经过实验,性能并没有改善多少,两个方案,一个是使用队列进行缓冲,
110 | > 开启一个 Go 协程写出数据,一个是不缓冲,直接开启多个 Go 协程进行写出数据。
111 | > 第一个方案中,性能反而下降了一倍,估计和内存分配有关,最重要的就是,如果队列还有缓冲数据,
112 | > 但是程序崩了,就有可能导致数据丢失,日志往往就需要最新最后的数据,而丢失的也正是最新最后的数据,
113 | > 所以这个方案直接否决。第二个方案中,性能几乎没有提升,而且导致日志输出的顺序不固定,也有可能丢失。
114 | > 综合上述,取消这个并发化日志输出的特性。
115 |
116 | * [ ] ~~给日志输出增加颜色显示~~
117 |
118 | > 取消颜色是因为考虑到线上生产环境主要使用文件,这个终端颜色显示的特性不是这么必须。
119 | > 如果要实现,还要针对不同的操作系统处理,代价大于价值,所以废弃这个新特性。
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | ## ✒ 历史版本的特性介绍 (Features in old versions)
2 |
3 | ### v1.8.3
4 |
5 | > 此版本发布于 2025-05-15
6 |
7 | * 继续调整代码
8 |
9 | ### v1.8.2
10 |
11 | > 此版本发布于 2025-03-15
12 |
13 | * 调整代码
14 |
15 | ### v1.8.1
16 |
17 | > 此版本发布于 2024-08-07
18 |
19 | * 快速时钟增加获取 nanos 的函数
20 |
21 | ### v1.8.0
22 |
23 | > 此版本发布于 2024-08-07
24 |
25 | * 提高单元测试覆盖率到 80%
26 | * 增加快速时钟,可以非常快速地查询时间
27 |
28 | ### v1.5.10
29 |
30 | > 此版本发布于 2024-01-19
31 |
32 | * v1.5.x 第一个正式版
33 |
34 | ### v1.5.9-alpha
35 |
36 | > 此版本发布于 2024-01-09
37 |
38 | * 去除对 \ 和 " 的转义
39 |
40 | ### v1.5.8-alpha
41 |
42 | > 此版本发布于 2023-12-27
43 |
44 | * 调整生产选项,去除 pid 的输出
45 |
46 | ### v1.5.7-alpha
47 |
48 | > 此版本发布于 2023-12-22
49 |
50 | * 增加包级别的 Sync 和 Close 函数
51 |
52 | ### v1.5.6-alpha
53 |
54 | > 此版本发布于 2023-12-21
55 |
56 | * 增加包级别的日志方法
57 | * PS: 周董的新歌《圣诞星》上线了,怎么说呢哈哈,周董的光芒还是留在回忆里面吧~
58 |
59 | ### v1.5.5-alpha
60 |
61 | > 此版本发布于 2023-12-20
62 |
63 | * 继续优化代码,包括设计和质量
64 | * 完善单元测试,提高覆盖率到 78%
65 |
66 | ### v1.5.4-alpha
67 |
68 | > 此版本发布于 2023-12-18
69 |
70 | * 增加一个高可读性且高性能的 handler 实现 —— TapeHandler
71 | * 新的 tape handler 性能提升了 33%,且 print 类函数性能提升了 50%
72 |
73 | ### v1.5.3-alpha
74 |
75 | > 此版本发布于 2023-12-13
76 |
77 | * 简化代码,去除冗余的一些东西
78 |
79 | ### v1.5.2-alpha
80 |
81 | > 此版本发布于 2023-12-13
82 |
83 | * 优化 handler 和 writer 设计
84 |
85 | ### v1.5.1-alpha
86 |
87 | > 此版本发布于 2023-12-12
88 |
89 | * 增加属性解析器适配功能
90 | * Logger 性能提升接近 20%
91 | * TextHandler 零内存分配
92 |
93 | ### v1.5.0-alpha
94 |
95 | > 此版本发布于 2023-12-11
96 |
97 | * 考虑结合 Go1.21 新加的 slog 包,做一些生产化的适配和功能
98 | * 增加 context 适配功能
99 | * 增加 Production 现成配置,搭配 Option 方便使用
100 | * 完善 Logger 相关的单元测试
101 | * 调整 writer 包代码,完善单元测试
102 | * 调整 rotate 包代码,完善单元测试
103 | * 完善 config 包功能和单元测试
104 | * 完善性能测试
105 | * 完善示例代码
106 |
107 | ### v1.2.1
108 |
109 | > 此版本发布于 2023-07-03
110 |
111 | * 调整 logPool 为指针引用,避免 Logger 结构复制引发 sync.Pool 的复制
112 |
113 | ### v1.2.0
114 |
115 | > 此版本发布于 2023-06-19
116 |
117 | * 增加 LogX 方法,方便使用 context 和 interceptor
118 | * 增加 time.Duration 相关方法
119 |
120 | ### v1.1.0
121 |
122 | > 此版本发布于 2023-03-24
123 |
124 | * 调整 global logger 的设计,去除 caller + 1 的代码
125 | * 完善单元测试
126 |
127 | ### v1.0.0
128 |
129 | > 此版本发布于 2023-01-21
130 |
131 | * 稳定的 API 版本
132 |
133 | ### v0.7.1
134 |
135 | > 此版本发布于 2022-12-21
136 |
137 | * 修复 extension file 文件冲突问题
138 |
139 | ### v0.7.0
140 |
141 | > 此版本发布于 2022-11-23
142 |
143 | * 调整 Error 的 API 设计
144 | * 增加 WithError 方法,代替 Err 方法
145 |
146 | ### v0.6.1
147 |
148 | > 此版本发布于 2022-10-24
149 |
150 | * 增加 errorKey 和 Err 方法,方便 Error 日志的输出
151 |
152 | ### v0.6.0
153 |
154 | > 此版本发布于 2022-10-08
155 |
156 | * 原谅我将仓库从组织中转移回个人。。因为组织没啥用。。
157 |
158 | ### v0.5.8
159 |
160 | > 此版本发布于 2022-10-01
161 |
162 | * 国庆节快乐!
163 |
164 | ### v0.5.7-alpha
165 |
166 | > 此版本发布于 2022-09-17
167 |
168 | * 这个周末,我开始了在声乐上的学习生涯,希望半年后的我会在声乐上有所进展!
169 |
170 | ### v0.5.6-alpha
171 |
172 | > 此版本发布于 2022-09-04
173 |
174 | * 继续完善单元测试
175 | * 继续优化代码设计
176 | * 修复了 config 的 unix 时间解析问题
177 | * 增加 appender.TextWith 函数,支持不转义 key 和 value 的值
178 |
179 | ### v0.5.5-alpha
180 |
181 | > 此版本发布于 2022-09-02
182 |
183 | * 大量完善单元测试
184 | * 大量优化代码设计
185 | * 修复 WithPID 在默认开启时不会添加 pid 的问题
186 |
187 | ### v0.5.4-alpha
188 |
189 | > 此版本发布于 2022-09-01
190 |
191 | * 大幅度优化 API 和代码(再忙也要学会优雅哦,哈哈哈)
192 | * 增加配置项解析功能,提供配置文件支持
193 | * 加入日志存活天数的特性
194 | * 加入日志存活个数的特性
195 | * 按时间、文件大小自动切割日志文件,并支持过期清理机制
196 |
197 | ### v0.5.3-alpha
198 |
199 | > 此版本发布于 2022-08-28
200 |
201 | * 调整包结构,优化代码
202 | * 增加配置文件解析机制
203 |
204 | ### v0.5.2-alpha
205 |
206 | > 此版本发布于 2022-08-27
207 |
208 | * 增加错误监控回调函数
209 |
210 | ### v0.5.1-alpha
211 |
212 | > 此版本发布于 2022-08-24
213 |
214 | * 提取时间函数,方便做时间查询优化
215 | * 优化 Json 方法的 Appender 实现
216 |
217 | ### v0.5.0-alpha
218 |
219 | > 此版本发布于 2022-08-22
220 |
221 | * 替换 End() 方法,使用 Log() 代替,代码可读性更高
222 | * 代码优化和重构,包括性能和逻辑上的优化,代码质量上的重构
223 | * 支持增加调用堆栈或者函数名的信息到日志
224 | * 增加批量写入的 Writer,区别于缓冲写入的 Writer,这个批量是按照数量进行缓冲的
225 |
226 | ### v0.4.22
227 |
228 | > 此版本发布于 2022-03-19
229 |
230 | * 前两个测试版本的正式发布版本
231 |
232 | ### v0.4.21-alpha
233 |
234 | > 此版本发布于 2022-02-27
235 |
236 | * 废弃 LoggerMaker
237 | * 加入全局日志方法
238 |
239 | ### v0.4.20-alpha
240 |
241 | > 此版本发布于 2022-02-23
242 |
243 | * 加入日志拦截器,支持从 context 注入键值对
244 | * 优化 Logger 字段结构,减少多余引用变量
245 | * 更改 LoggerMaker 为 LoggerCreator,并废弃 LoggerMaker
246 | * 更改 MarshalJson 为 MarshalToJson,并废弃 MarshalJson
247 |
248 | ### v0.4.19
249 |
250 | > 此版本发布于 2022-02-13
251 |
252 | * 给 log 加入 Json 方法
253 |
254 | ### v0.4.18
255 |
256 | > 此版本发布于 2022-02-08
257 |
258 | * 修复 Caller Depth 不生效的问题
259 |
260 | ### v0.4.17
261 |
262 | > 此版本发布于 2022-02-08
263 |
264 | * 修复 Version 错误的问题
265 | * 修复 Printf、Print、Println 方法下 level 错误的问题
266 | * 加入 WithPid 方法
267 | * 完善 PrintLevel 相关的逻辑
268 |
269 | ### v0.4.16
270 |
271 | > 此版本发布于 2022-02-02
272 |
273 | * 庆祝下仓库转移成功!!!
274 |
275 | ### v0.4.15
276 |
277 | > 此版本发布于 2022-02-01
278 |
279 | * 祝大家新春快乐 :)
280 |
281 | ### v0.4.14-alpha
282 |
283 | > 此版本发布于 2022-01-31
284 |
285 | * 兼容 log 包部分方法,比如 Printf
286 |
287 | ### v0.4.13-alpha
288 |
289 | > 此版本发布于 2022-01-29
290 |
291 | * Log 增加 Caller 方法,方便某些场景下获取调用信息
292 |
293 | ### v0.4.12-alpha
294 |
295 | > 此版本发布于 2022-01-29
296 |
297 | * depth 放开,可以支持用户包裹
298 |
299 | ### v0.4.11
300 |
301 | > 此版本发布于 2021-11-29
302 |
303 | * 修改 LoggerMaker 接口 params 为可变参数
304 |
305 | ### v0.4.10
306 |
307 | > 此版本发布于 2021-11-01
308 |
309 | * 增加 LoggerMaker 扩展功能
310 |
311 | ### v0.4.9-alpha
312 |
313 | > 此版本发布于 2021-10-27
314 |
315 | * 修复 TextAppender 的 AppendErrors 和 AppendStringers 追加问题
316 | * 完善单元测试
317 |
318 | ### v0.4.8-alpha
319 |
320 | > 此版本发布于 2021-10-10
321 |
322 | * 修改 sync.Mutex 的使用方式
323 | * 完善单元测试
324 |
325 | ### v0.4.7-alpha
326 |
327 | > 此版本发布于 2021-09-27
328 |
329 | * 加入 Context 机制,更优雅地使用日志,并支持业务域划分
330 |
331 | ### v0.4.6-alpha
332 |
333 | > 此版本发布于 2021-09-08
334 |
335 | * 调整转义字符显示样式
336 |
337 | ### v0.4.5-alpha
338 |
339 | > 此版本发布于 2021-09-06
340 |
341 | * 修复 Appender 接口实现类检验失效问题
342 | * 调整 textAppender 的字段分隔符为 |
343 | * 去除 signal 机制的 TODO
344 |
345 | ### v0.4.4-alpha
346 |
347 | > 此版本发布于 2021-08-10
348 |
349 | * 修复 WithWriter 的顺序问题
350 |
351 | ### v0.4.3-alpha
352 |
353 | > 此版本发布于 2021-08-10
354 |
355 | * 优化部分 log 设计
356 | * 增加多级别追加器机制
357 | * 增加多级别写出器机制
358 | * 完善文档注释和单元测试
359 |
360 | ### v0.4.2-alpha
361 |
362 | > 此版本发布于 2021-08-01
363 |
364 | * 修复部分问题
365 | * 完善文档注释
366 |
367 | ### v0.4.1-alpha
368 |
369 | > 此版本发布于 2021-07-15
370 |
371 | * 改造 API 使用体验,加入结构化日志功能
372 | * Options 机制引入,支持多种创建选项
373 | * 改造 Encoder 为 Appender,直接追加字节数据
374 | * 提供高级调优配置能力,贴合业务进行设置
375 |
376 | ### v0.4.0-alpha
377 |
378 | > 此版本发布于 2021-06-08
379 |
380 | * 大幅度重构设计,精简功能,后续继续完善原有功能
381 | * 重构 Encoder,加入缓冲区对象池
382 | * 引入新的日志输出器,并且带有缓冲和异步写功能
383 |
384 | ### v0.3.3
385 |
386 | > 此版本发布于 2021-01-12
387 |
388 | * 去除了 DebugF 一类格式化的 API,统一使用 Debug 一类 API
389 |
390 | ### v0.3.2
391 |
392 | > 此版本发布于 2020-12-24
393 |
394 | * 废弃了 files 包
395 | * 完善文档,第一个 v0.3.x 正式版
396 | * 最后,祝大家平安夜、圣诞节快乐!
397 |
398 | ### v0.3.1-alpha
399 |
400 | > 此版本发布于 2020-12-13
401 |
402 | * 日志文件自动分割处理,支持 Checker 机制检查是否需要分割
403 | * 内置三种 Checker,分别是 TimeChecker,SizeChecker,CountChecker
404 | * 多种检查器可以叠加效果,只要其中有一个达到分割的条件都会进行分割
405 |
406 | ### v0.3.0-alpha
407 |
408 | > 此版本发布于 2020-11-28
409 |
410 | * 大幅度重构版本,废除了 handler 设计
411 | * 暂不支持配置文件,正式版会支持
412 | * v0.3.x 的第一个体验版
413 |
414 | ### v0.2.10
415 |
416 | > 此版本发布于 2020-08-22
417 |
418 | * 完善配置文件注释,目前是 // 和 # 都支持,后续将只支持 //
419 | * 完善配置文件格式,目前的 Json 配置是不用 {} 包裹也可以的,后续将必须使用 {} 进行包裹
420 |
421 | ### v0.2.9
422 |
423 | > 此版本发布于 2020-08-08
424 |
425 | * 日志文件输出会自动创建父级不存在的目录
426 |
427 | ### v0.2.8
428 |
429 | > 此版本发布于 2020-07-30
430 |
431 | * 去掉 JsonEncoder 的 key 空格
432 |
433 | ### v0.2.7
434 |
435 | > 此版本发布于 2020-06-25
436 |
437 | * 修复了 DefaultNameGenerator 可能产生重复文件名的 bug
438 |
439 | ### v0.2.6-alpha
440 |
441 | > 此版本发布于 2020-06-24
442 |
443 | * 对 writer 包进行重构,改名为 files 包
444 | * 废弃了原 writer 包的 NewFile 方法,并使用同包下的 CreateFileOf 代替
445 | * 引入 NameGenerator 组件
446 | * 修改 NewDurationRollingHandler 的参数顺序
447 | * 修改 NewSizeRollingHandler 的参数顺序
448 |
449 | ### v0.2.5-alpha
450 |
451 | > 此版本发布于 2020-06-08
452 |
453 | * 加入之前被移除的特性 - 可变长参数列表的日志输出支持,主要可以使用格式化字符串进行多参数传递
454 |
455 | ### v0.2.4
456 |
457 | > 此版本发布于 2020-05-27
458 |
459 | * 新增屏蔽某个日志级别的日志处理器
460 | * 修正某些文档的语法问题
461 | * 修复部分单元测试引用外部文件(比如 _examples 中的文件)的问题
462 |
463 | ### v0.2.3
464 |
465 | > 此版本发布于 2020-05-01
466 |
467 | * 祝大家五一劳动节快乐!
468 |
469 | ### v0.2.2-alpha
470 |
471 | > 此版本发布于 2020-04-28
472 |
473 | * 改造全局使用的 logger,可以使用一个默认的配置文件来初始化全局 logger,方便使用
474 | * 增加 levelBasedHandler,里面是不同日志级别的日志处理器包装器,可以传一堆的 handler 进去
475 |
476 | ### v0.2.1-alpha
477 |
478 | > 此版本发布于 2020-04-27
479 |
480 | * 将 console handler 简化,目前使用 RegisterHandler 构造
481 | * 从 file handler 中抽取出 duration rolling 和 size rolling 两个日志处理器
482 | * 屏蔽了 HandlerOf 和 EncoderOf,只暴露特定的 API
483 | * 新增 TextEncoder 和 JsonEncoder 两个方法,可以获取到具体的日志编码器
484 | * 新增 NewConsoleHandler 和 NewFileHandler,分别对应控制台和文件日志处理器
485 | * 新增 NewDurationRollingHandler 和 NewSizeRollingHandler,分别对应时间间隔滚动和文件大小滚动的日志处理器
486 | * 删除了大量创建 Logger 的方法,这些方法会让人看起来很复杂很繁琐
487 | * 去除原有 Config 加 fileConfig 的配置设计,现在直接使用一个映射配置,然后组装成需要的参数
488 |
489 | ### v0.2.0-alpha
490 |
491 | > 此版本发布于 2020-04-24
492 |
493 | * 将 wrapper 修改为 writer
494 | * 剔除了 default handler 和 json handler,整合进 standard handler 中
495 | * 提取出一个 encoder,方便内置处理器引用
496 | * 加入 console handler,专门负责输出到控制台的日志处理器
497 | * 加入 file handler,专门负责文件相关的日志处理器,包含时间滚动和大小滚动和不滚动的功能
498 |
499 | ### v0.1.5
500 |
501 | > 此版本发布于 2020-04-19
502 |
503 | * 完善 Json 处理器没有做字符转义的修复方案,详情查询 [issue/1](https://github.com/FishGoddess/logit/issues/1)
504 |
505 | ### v0.1.4
506 |
507 | > 此版本发布于 2020-04-10
508 |
509 | * 紧急修复 Json 处理器没有做字符转义的 bug,详情查询 [issue/1](https://github.com/FishGoddess/logit/issues/1)
510 |
511 | ### v0.1.3
512 |
513 | > 此版本发布于 2020-04-05
514 |
515 | * 增加配置文件中是否开启文件信息记录的选项
516 |
517 | ### v0.1.2
518 |
519 | > 此版本发布于 2020-03-30
520 |
521 | * 加入配置文件的支持,以近似 Json 格式的配置文件来增加日志记录的灵活性
522 | * 修复 Logger 中 DebugFunc,InfoFunc,WarnFunc,ErrorFunc 等几个方法的文件信息错误问题
523 | * 修复 logit 中 DebugFunc,InfoFunc,WarnFunc,ErrorFunc 等几个方法的文件信息错误问题
524 |
525 | ### v0.1.1-alpha
526 |
527 | > 此版本发布于 2020-03-29
528 |
529 | * 再次对 Handler 进行重构,尽量优化 Logger 的设计
530 | * 去除 Encoder,减少多余的设计,轻量化 Logger
531 | * 取消时间缓存机制,减少并发竞争性
532 | * 优化 releaseLog 的 extra 内存分配
533 | * 加入 FileConfig,为后续支持配置文件做准备
534 |
535 | ### v0.1.0-alpha
536 |
537 | > 此版本发布于 2020-03-27
538 |
539 | * 重新设计 Logger,主要是轻量化处理和重构 handler 的设计
540 | * 增加 Encoder 接口,方便用户扩展 Logger,并内置 Json 编码器
541 | * Json 编码器允许时间不做格式化,使用 Unix 形式处理时间,方便解析处理
542 |
543 | ### v0.0.11
544 |
545 | > 此版本发布于 2020-03-23
546 |
547 | * 支持日志输出为 Json 形式,通过增加 JSON 日志处理器实现
548 | * 使用时间缓存机制优化时间格式化操作性能消耗过多的问题,性能再次提升 50%
549 |
550 | ### v0.0.10
551 |
552 | > 此版本发布于 2020-03-10
553 |
554 | * 扩展了 Logger 的方法,可以获取到内部的属性,为日志处理器做准备
555 | * 支持创建 Logger 对象之后修改它的输出源 writer(这是个之前被遗漏的功能特性哈哈)
556 | * 调整了内部 log 方法的锁机制,使用类似于写时复制的方式释放日志输出的并发性
557 |
558 | ### v0.0.9
559 |
560 | > 此版本发布于 2020-03-09
561 |
562 | * 支持日志输出函数,日志信息可以是一个返回 string 的函数
563 | * 公开 PrefixOf 方法,方便用户自定义处理器的时候获取日志级别字符串
564 |
565 | ### v0.0.8
566 |
567 | > 此版本发布于 2020-03-08
568 |
569 | * 进行第一次性能优化,性能相比之前版本提升 30%
570 | * 取消占位符功能,由于这个功能的实现需要对类型进行反射检测,非常消耗性能
571 | * 取消 fmt 包的使用,经过性能检测,发现 fmt 包中存在大量使用反射的耗时行为
572 |
573 | ### v0.0.7
574 |
575 | > 此版本发布于 2020-03-06
576 |
577 | * 重构日志输出的模块,抛弃了标准库的 log 设计
578 | * 增加日志处理器模块,支持用户自定义日志处理逻辑,大大地提高了扩展能力
579 | * 支持不输出文件信息,避免 runtime.Caller 方法的调用,大大地提高了性能
580 | * 支持调整时间格式化输出,让用户自定义时间输出的格式
581 |
582 | ### v0.0.6
583 |
584 | > 此版本发布于 2020-03-05
585 |
586 | * 支持按照文件大小自动划分日志文件
587 | * 修复 nextFilename 中随机数生成重复的问题,设置了纳秒时钟作为种子
588 |
589 | ### v0.0.5
590 |
591 | > 此版本发布于 2020-03-04
592 |
593 | * 支持将日志输出到文件
594 | * 支持按照时间间隔自动划分日志文件
595 |
596 | ### v0.0.4
597 |
598 | > 此版本发布于 2020-03-03
599 |
600 | * 修改 **LogLevel** 类型为 **LoggerLevel**,命名更符合意义
601 | * 更改了部分源文件的命名,也是为了更符合实际的意义
602 |
603 | ### v0.0.3
604 |
605 | > 此版本发布于 2020-03-02
606 |
607 | * 让信息输出支持占位符,比如 %d 之类的
608 | * 修复 Logit 日志调用方法的调用深度问题,之前直接通过 logit 调用时文件信息会显示错误
609 |
610 | ### v0.0.2
611 |
612 | > 此版本发布于 2020-03-01
613 |
614 | * 扩展 Logger 的使用方法,主要是创建日志记录器一类的方法
615 | * 扩展 logit 的全局使用方法,增加一个默认的日志记录器
616 | * 支持更改日志级别,参考 Logger#ChangeLevelTo 方法
617 | * 修复 Logger#log 方法中漏加读锁导致并发安全的问题
618 |
619 | ### v0.0.1
620 |
621 | > 此版本发布于 2020-02-29
622 |
623 | * 实现最简单的日志输出功能
624 | * 支持四种日志级别:_debug_, _info_, _warn_, _error_。
625 | * 对应四种日志级别分别有四个方法。
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [2020] [FishGoddess]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test bench fmt
2 |
3 | all: test bench
4 |
5 | test:
6 | go test -cover -count=1 -test.cpu=1 ./...
7 |
8 | bench:
9 | go test -v ./_examples/performance_test.go -bench=^BenchmarkLogit -benchtime=1s
10 |
11 | fmt:
12 | go fmt ./...
--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
1 | # 📝 logit
2 |
3 | [](https://pkg.go.dev/github.com/FishGoddess/logit)
4 | [](https://www.apache.org/licenses/LICENSE-2.0.html)
5 | [](_icons/coverage.svg)
6 | 
7 |
8 | **logit** is a level-based, high-performance and pure-structured logger for all [GoLang](https://golang.org)
9 | applications.
10 |
11 | [阅读中文版的 Read me](./README.md)
12 |
13 | ### 🥇 Features
14 |
15 | * Based on Handler in Go, and provide a better performance.
16 | * Level-based logging, and there are four levels to use: debug, info, warn, error.
17 | * Key-Value structured log supports, also supporting format.
18 | * Support logging as Text/Json string, by using provided appender.
19 | * Asynchronous write back supports, providing high-performance Buffer writer to avoid IO accessing.
20 | * Provide global optimized settings, let some settings feet your business.
21 | * Every level has its own appender and writer, separating process error logs is recommended.
22 | * Context binding supports, using logger is more elegant.
23 | * Configuration plugins supports, ex: yaml plugin can create logger from yaml configuration file.
24 | * Interceptor supports which can inject values from context.
25 | * Error handling supports which can let you count errors and report them.
26 | * Rotate file supports, clean automatically if files are aged or too many.
27 | * Config file supports, such as json/yaml/toml/bson.
28 |
29 | _Check [HISTORY.md](./HISTORY.md) and [FUTURE.md](./FUTURE.md) to know about more information._
30 |
31 | > We redesigned api of logit after v1.5.0 because Go1.21 introduced slog package.
32 |
33 | ### 🚀 Installation
34 |
35 | ```bash
36 | $ go get -u github.com/FishGoddess/logit
37 | ```
38 |
39 | ### 📖 Examples
40 |
41 | ```go
42 | package main
43 |
44 | import (
45 | "context"
46 | "fmt"
47 |
48 | "github.com/FishGoddess/logit"
49 | )
50 |
51 | func main() {
52 | // Use default logger to log.
53 | // By default, logs will be output to stdout.
54 | // logit.Default() returns the default logger, but we also provide some common logging functions.
55 | logit.Info("hello from logit", "key", 123)
56 | logit.Default().Info("hello from logit", "key", 123)
57 |
58 | // Use a new logger to log.
59 | // By default, logs will be output to stdout.
60 | logger := logit.NewLogger()
61 |
62 | logger.Debug("new version of logit", "version", "1.5.0-alpha", "date", 20231122)
63 | logger.Error("new version of logit", "version", "1.5.0-alpha", "date", 20231122)
64 |
65 | type user struct {
66 | ID int64 `json:"id"`
67 | Name string `json:"name"`
68 | }
69 |
70 | u := user{123456, "fishgoddess"}
71 | logger.Info("user information", "user", u, "pi", 3.14)
72 |
73 | // Yep, I know you want to output logs to a file, try WithFile option.
74 | // The path in WithFile is where the log file will be stored.
75 | // Also, it's a good choice to call logger.Close() when program shutdown.
76 | logger = logit.NewLogger(logit.WithFile("./logit.log"))
77 | defer logger.Close()
78 |
79 | logger.Info("check where I'm logged", "file", "logit.log")
80 |
81 | // What if I want to use default logger and output logs to a file? Try SetDefault.
82 | // It sets a logger to default and you can use it by package functions or Default().
83 | logit.SetDefault(logger)
84 | logit.Warn("this is from default logger", "pi", 3.14, "default", true)
85 |
86 | // If you want to change level of logger to info, try WithInfoLevel.
87 | // Other levels is similar to info level.
88 | logger = logit.NewLogger(logit.WithInfoLevel())
89 |
90 | logger.Debug("debug logs will be ignored")
91 | logger.Info("info logs can be logged")
92 |
93 | // If you want to pass logger by context, use NewContext and FromContext.
94 | ctx := logit.NewContext(context.Background(), logger)
95 |
96 | logger = logit.FromContext(ctx)
97 | logger.Info("logger from context", "from", "context")
98 |
99 | // Don't want to panic when new a logger? Try NewLoggerGracefully.
100 | logger, err := logit.NewLoggerGracefully(logit.WithFile(""))
101 | if err != nil {
102 | fmt.Println("new logger gracefully failed:", err)
103 | }
104 | }
105 | ```
106 |
107 | _More examples can be found in [_examples](./_examples)._
108 |
109 | ### 🔥 Benchmarks
110 |
111 | ```bash
112 | $ make bench
113 | ```
114 |
115 | ```bash
116 | goos: linux
117 | goarch: amd64
118 | cpu: AMD EPYC 7K62 48-Core Processor
119 |
120 | BenchmarkLogitLogger-2 1486184 810 ns/op 0 B/op 0 allocs/op
121 | BenchmarkLogitLoggerTextHandler-2 1000000 1080 ns/op 0 B/op 0 allocs/op
122 | BenchmarkLogitLoggerJsonHandler-2 847864 1393 ns/op 120 B/op 3 allocs/op
123 | BenchmarkLogitLoggerPrint-2 1222302 981 ns/op 48 B/op 1 allocs/op
124 | BenchmarkSlogLoggerTextHandler-2 725522 1629 ns/op 0 B/op 0 allocs/op
125 | BenchmarkSlogLoggerJsonHandler-2 583214 2030 ns/op 120 B/op 3 allocs/op
126 | BenchmarkZeroLogLogger-2 1929276 613 ns/op 0 B/op 0 allocs/op
127 | BenchmarkZapLogger-2 976855 1168 ns/op 216 B/op 2 allocs/op
128 | BenchmarkLogrusLogger-2 231723 4927 ns/op 2080 B/op 32 allocs/op
129 |
130 | BenchmarkLogitFile-2 624774 1935 ns/op 0 B/op 0 allocs/op
131 | BenchmarkLogitFileWithBuffer-2 1378076 873 ns/op 0 B/op 0 allocs/op
132 | BenchmarkLogitFileWithBatch-2 1367479 883 ns/op 0 B/op 0 allocs/op
133 | BenchmarkSlogFile-2 407590 2944 ns/op 0 B/op 0 allocs/op
134 | BenchmarkZeroLogFile-2 634375 1810 ns/op 0 B/op 0 allocs/op
135 | BenchmarkZapFile-2 382790 2641 ns/op 216 B/op 2 allocs/op
136 | BenchmarkLogrusFile-2 174944 6491 ns/op 2080 B/op 32 allocs/op
137 | ```
138 |
139 | > Notice: WithBuffer and WithBatch are using buffer writer and batch writer.
140 |
141 | > Benchmarks: [_examples/performance_test.go](./_examples/performance_test.go)
142 |
143 | ### 👥 Contributing
144 |
145 | If you find that something is not working as expected please open an _**issue**_.
146 |
147 | [](https://star-history.com/#fishgoddess/logit&Date)
148 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 📝 logit
2 |
3 | [](https://pkg.go.dev/github.com/FishGoddess/logit)
4 | [](https://www.apache.org/licenses/LICENSE-2.0.html)
5 | [](_icons/coverage.svg)
6 | 
7 |
8 | **logit** 是一个基于级别控制的高性能纯结构化日志库,可以应用于所有的 [GoLang](https://golang.org) 应用程序中。
9 |
10 | [Read me in English](./README.en.md)
11 |
12 | ### 🥇 功能特性
13 |
14 | * 兼容标准库 Handler 的扩展设计,并且提供了更高的性能。
15 | * 支持日志级别控制,一共有四个日志级别,分别是 debug,info,warn,error。
16 | * 支持键值对形式的结构化日志记录,同时对格式化操作也有支持。
17 | * 支持以 Text/Json 形式输出日志信息,方便对日志进行解析。
18 | * 支持异步回写日志,提供高性能缓冲写出器模块,减少 IO 的访问次数。
19 | * 提供调优使用的全局配置,对一些高级配置更贴合实际业务的需求。
20 | * 加入 Context 机制,更优雅地使用日志,并支持业务分组划分。
21 | * 支持拦截器模式,可以从 context 注入外部常量或变量,简化日志输出流程。
22 | * 支持错误监控,可以很方便地进行错误统计和告警。
23 | * 支持日志按大小自动分割,并支持按照时间和数量自动清理。
24 | * 支持多种配置文件序列化成 option,比如 json/yaml/toml/bson,然后创建日志记录器。
25 |
26 | _历史版本的特性请查看 [HISTORY.md](./HISTORY.md)。未来版本的新特性和计划请查看 [FUTURE.md](./FUTURE.md)。_
27 |
28 | > 由于 Go1.21 增加了 slog 日志包,基本确定了 Go 风格的日志 API,所以 logit v1.5.0 版本开始也调整为类似的风格,并且提供更多的功能和更高的性能。
29 |
30 | ### 🚀 安装方式
31 |
32 | ```bash
33 | $ go get -u github.com/FishGoddess/logit
34 | ```
35 |
36 | ### 📖 参考案例
37 |
38 | ```go
39 | package main
40 |
41 | import (
42 | "context"
43 | "fmt"
44 |
45 | "github.com/FishGoddess/logit"
46 | )
47 |
48 | func main() {
49 | // Use default logger to log.
50 | // By default, logs will be output to stdout.
51 | // logit.Default() returns the default logger, but we also provide some common logging functions.
52 | logit.Info("hello from logit", "key", 123)
53 | logit.Default().Info("hello from logit", "key", 123)
54 |
55 | // Use a new logger to log.
56 | // By default, logs will be output to stdout.
57 | logger := logit.NewLogger()
58 |
59 | logger.Debug("new version of logit", "version", "1.5.0-alpha", "date", 20231122)
60 | logger.Error("new version of logit", "version", "1.5.0-alpha", "date", 20231122)
61 |
62 | type user struct {
63 | ID int64 `json:"id"`
64 | Name string `json:"name"`
65 | }
66 |
67 | u := user{123456, "fishgoddess"}
68 | logger.Info("user information", "user", u, "pi", 3.14)
69 |
70 | // Yep, I know you want to output logs to a file, try WithFile option.
71 | // The path in WithFile is where the log file will be stored.
72 | // Also, it's a good choice to call logger.Close() when program shutdown.
73 | logger = logit.NewLogger(logit.WithFile("./logit.log"))
74 | defer logger.Close()
75 |
76 | logger.Info("check where I'm logged", "file", "logit.log")
77 |
78 | // What if I want to use default logger and output logs to a file? Try SetDefault.
79 | // It sets a logger to default and you can use it by package functions or Default().
80 | logit.SetDefault(logger)
81 | logit.Warn("this is from default logger", "pi", 3.14, "default", true)
82 |
83 | // If you want to change level of logger to info, try WithInfoLevel.
84 | // Other levels is similar to info level.
85 | logger = logit.NewLogger(logit.WithInfoLevel())
86 |
87 | logger.Debug("debug logs will be ignored")
88 | logger.Info("info logs can be logged")
89 |
90 | // If you want to pass logger by context, use NewContext and FromContext.
91 | ctx := logit.NewContext(context.Background(), logger)
92 |
93 | logger = logit.FromContext(ctx)
94 | logger.Info("logger from context", "from", "context")
95 |
96 | // Don't want to panic when new a logger? Try NewLoggerGracefully.
97 | logger, err := logit.NewLoggerGracefully(logit.WithFile(""))
98 | if err != nil {
99 | fmt.Println("new logger gracefully failed:", err)
100 | }
101 | }
102 | ```
103 |
104 | _更多使用案例请查看 [_examples](./_examples) 目录。_
105 |
106 | ### 🔥 性能测试
107 |
108 | ```bash
109 | $ make bench
110 | ```
111 |
112 | ```bash
113 | goos: linux
114 | goarch: amd64
115 | cpu: AMD EPYC 7K62 48-Core Processor
116 |
117 | BenchmarkLogitLogger-2 1486184 810 ns/op 0 B/op 0 allocs/op
118 | BenchmarkLogitLoggerTextHandler-2 1000000 1080 ns/op 0 B/op 0 allocs/op
119 | BenchmarkLogitLoggerJsonHandler-2 847864 1393 ns/op 120 B/op 3 allocs/op
120 | BenchmarkLogitLoggerPrint-2 1222302 981 ns/op 48 B/op 1 allocs/op
121 | BenchmarkSlogLoggerTextHandler-2 725522 1629 ns/op 0 B/op 0 allocs/op
122 | BenchmarkSlogLoggerJsonHandler-2 583214 2030 ns/op 120 B/op 3 allocs/op
123 | BenchmarkZeroLogLogger-2 1929276 613 ns/op 0 B/op 0 allocs/op
124 | BenchmarkZapLogger-2 976855 1168 ns/op 216 B/op 2 allocs/op
125 | BenchmarkLogrusLogger-2 231723 4927 ns/op 2080 B/op 32 allocs/op
126 |
127 | BenchmarkLogitFile-2 624774 1935 ns/op 0 B/op 0 allocs/op
128 | BenchmarkLogitFileWithBuffer-2 1378076 873 ns/op 0 B/op 0 allocs/op
129 | BenchmarkLogitFileWithBatch-2 1367479 883 ns/op 0 B/op 0 allocs/op
130 | BenchmarkSlogFile-2 407590 2944 ns/op 0 B/op 0 allocs/op
131 | BenchmarkZeroLogFile-2 634375 1810 ns/op 0 B/op 0 allocs/op
132 | BenchmarkZapFile-2 382790 2641 ns/op 216 B/op 2 allocs/op
133 | BenchmarkLogrusFile-2 174944 6491 ns/op 2080 B/op 32 allocs/op
134 | ```
135 |
136 | > 注:WithBuffer 和 WithBatch 分别是使用了缓冲器和批量写入的方式进行测试。
137 |
138 | > 测试文件:[_examples/performance_test.go](./_examples/performance_test.go)
139 |
140 | ### 👥 贡献者
141 |
142 | 如果您觉得 logit 缺少您需要的功能,请不要犹豫,马上参与进来,发起一个 _**issue**_。
143 |
144 | [](https://star-history.com/#fishgoddess/logit&Date)
145 |
--------------------------------------------------------------------------------
/_examples/basic.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "context"
19 | "fmt"
20 |
21 | "github.com/FishGoddess/logit"
22 | )
23 |
24 | func main() {
25 | // Use default logger to log.
26 | // By default, logs will be output to stdout.
27 | // logit.Default() returns the default logger, but we also provide some common logging functions.
28 | logit.Info("hello from logit", "key", 123)
29 | logit.Default().Info("hello from logit", "key", 123)
30 |
31 | // Use a new logger to log.
32 | // By default, logs will be output to stdout.
33 | logger := logit.NewLogger()
34 |
35 | logger.Debug("new version of logit", "version", "1.5.0-alpha", "date", 20231122)
36 | logger.Error("new version of logit", "version", "1.5.0-alpha", "date", 20231122)
37 |
38 | type user struct {
39 | ID int64 `json:"id"`
40 | Name string `json:"name"`
41 | }
42 |
43 | u := user{123456, "fishgoddess"}
44 | logger.Info("user information", "user", u, "pi", 3.14)
45 |
46 | // Yep, I know you want to output logs to a file, try WithFile option.
47 | // The path in WithFile is where the log file will be stored.
48 | // Also, it's a good choice to call logger.Close() when program shutdown.
49 | logger = logit.NewLogger(logit.WithFile("./logit.log"))
50 | defer logger.Close()
51 |
52 | logger.Info("check where I'm logged", "file", "logit.log")
53 |
54 | // What if I want to use default logger and output logs to a file? Try SetDefault.
55 | // It sets a logger to default and you can use it by package functions or Default().
56 | logit.SetDefault(logger)
57 | logit.Warn("this is from default logger", "pi", 3.14, "default", true)
58 |
59 | // If you want to change level of logger to info, try WithInfoLevel.
60 | // Other levels is similar to info level.
61 | logger = logit.NewLogger(logit.WithInfoLevel())
62 |
63 | logger.Debug("debug logs will be ignored")
64 | logger.Info("info logs can be logged")
65 |
66 | // If you want to pass logger by context, use NewContext and FromContext.
67 | ctx := logit.NewContext(context.Background(), logger)
68 |
69 | logger = logit.FromContext(ctx)
70 | logger.Info("logger from context", "from", "context")
71 |
72 | // Don't want to panic when new a logger? Try NewLoggerGracefully.
73 | logger, err := logit.NewLoggerGracefully(logit.WithFile(""))
74 | if err != nil {
75 | fmt.Println("new logger gracefully failed:", err)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/_examples/config.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "encoding/json"
19 | "os"
20 |
21 | "github.com/FishGoddess/logit"
22 | "github.com/FishGoddess/logit/extension/config"
23 | )
24 |
25 | // newConfig reads config from a json file.
26 | func newConfig() (*config.Config, error) {
27 | conf := new(config.Config)
28 |
29 | bs, err := os.ReadFile("config.json")
30 | if err != nil {
31 | return nil, err
32 | }
33 |
34 | err = json.Unmarshal(bs, conf)
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | return conf, err
40 | }
41 |
42 | func main() {
43 | // We know you may use a configuration file in your program to setup some resources including logging.
44 | // After thinking about serval kinds of configurations like "yaml", "toml" and "json", we decide to support all of them.
45 | // As you can see, there are many tags on Config's fields like "yaml" and "toml", so you can unmarshal to a config from one of them.
46 | conf, err := newConfig()
47 | if err != nil {
48 | panic(err)
49 | }
50 |
51 | opts, err := conf.Options()
52 | if err != nil {
53 | panic(err)
54 | }
55 |
56 | // Use options to create a logger.
57 | logger := logit.NewLogger(opts...)
58 | defer logger.Close()
59 |
60 | logger.Info("logging from config", "conf", conf)
61 | }
62 |
--------------------------------------------------------------------------------
/_examples/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "level": "debug",
3 | "handler": "tape",
4 | "writer": {
5 | "target": "logit.log",
6 | "file_rotate": true,
7 | "file_max_size": "1GB",
8 | "file_max_age": "7d",
9 | "file_max_backups": 30,
10 | "buffer_size": "64KB",
11 | "batch_size": 16
12 | },
13 | "with_source": false,
14 | "with_pid": true,
15 | "sync_timer": "1s"
16 | }
--------------------------------------------------------------------------------
/_examples/context.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "context"
19 |
20 | "github.com/FishGoddess/logit"
21 | )
22 |
23 | func main() {
24 | // We provide a way for getting logger from a context.
25 | // By default, the default logger will be returned if there is no logit.Logger in context.
26 | ctx := context.Background()
27 |
28 | logger := logit.FromContext(ctx)
29 | logger.Debug("logger from context debug")
30 |
31 | if logger == logit.Default() {
32 | logger.Info("logger from context is default logger")
33 | }
34 |
35 | // Use NewContext to set a logger to context.
36 | // We use WithGroup here to make a difference to default logger.
37 | logger = logit.NewLogger().WithGroup("context").With("user_id", 123456)
38 | ctx = logit.NewContext(ctx, logger)
39 |
40 | // Then you can get the logger from context.
41 | logger = logit.FromContext(ctx)
42 | logger.Debug("logger from context debug", "key", "value")
43 | }
44 |
--------------------------------------------------------------------------------
/_examples/default.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "log/slog"
20 | "time"
21 |
22 | "github.com/FishGoddess/logit"
23 | "github.com/FishGoddess/logit/defaults"
24 | )
25 |
26 | func main() {
27 | // We set a defaults package that setups all shared fields.
28 | // For example, if you want to customize the time getter:
29 | defaults.CurrentTime = func() time.Time {
30 | // Return a fixed time for example.
31 | return time.Unix(666, 0).In(time.Local)
32 | }
33 |
34 | logit.Print("println log is info level")
35 |
36 | // If you want change the level of old-school logging methods:
37 | defaults.LevelPrint = slog.LevelDebug
38 |
39 | logit.Print("println log is debug level now")
40 |
41 | // More fields see defaults package.
42 | defaults.HandleError = func(label string, err error) {
43 | fmt.Printf("%s: %+n\n", label, err)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/_examples/file.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "github.com/FishGoddess/logit"
19 | "github.com/FishGoddess/logit/rotate"
20 | )
21 |
22 | func main() {
23 | // AS we know, you can use WithFile to output logs to a file.
24 | logger := logit.NewLogger(logit.WithFile("logit.log"))
25 | logger.Debug("debug to file")
26 |
27 | // However, a single file stored all logs isn't enough in production.
28 | // Sometimes we want a log file has a limit size and count of files not greater than a number.
29 | // So we provide a rotate file to do this thing.
30 | logger = logit.NewLogger(logit.WithRotateFile("logit.log"))
31 | defer logger.Close()
32 |
33 | logger.Debug("debug to rotate file")
34 |
35 | // Maybe you have noticed that WithRotateFile can pass some rotate.Option.
36 | // These options are used to setup the rotate file.
37 | opts := []rotate.Option{
38 | rotate.WithMaxSize(128 * rotate.MB),
39 | rotate.WithMaxAge(30 * rotate.Day),
40 | rotate.WithMaxBackups(60),
41 | }
42 |
43 | logger = logit.NewLogger(logit.WithRotateFile("logit.log", opts...))
44 | defer logger.Close()
45 |
46 | logger.Debug("debug to rotate file with rotate options")
47 |
48 | // See rotate.File if you want to use this magic in other scenes.
49 | file, err := rotate.New("logit.log")
50 | if err != nil {
51 | panic(err)
52 | }
53 |
54 | defer file.Close()
55 | }
56 |
--------------------------------------------------------------------------------
/_examples/handler.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "io"
19 | "log/slog"
20 |
21 | "github.com/FishGoddess/logit"
22 | "github.com/FishGoddess/logit/handler"
23 | )
24 |
25 | func main() {
26 | // By default, logit uses tape handler to output logs.
27 | logger := logit.NewLogger()
28 | logger.Info("default handler logging")
29 |
30 | // You can change it to other handlers by options.
31 | // For example, use json handler:
32 | logger = logit.NewLogger(logit.WithJsonHandler())
33 | logger.Info("using json handler")
34 |
35 | // Or you want to use customized handlers, try Register.
36 | newHandler := func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
37 | return slog.NewTextHandler(w, opts)
38 | }
39 |
40 | if err := handler.Register("demo", newHandler); err != nil {
41 | panic(err)
42 | }
43 |
44 | logger = logit.NewLogger(logit.WithHandler("demo"))
45 | logger.Info("using demo handler")
46 |
47 | // As you can see, our handler is slog's handler, so you can use any handlers implement this interface.
48 | newHandler = func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
49 | return slog.NewJSONHandler(w, opts)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/_examples/logger.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "io"
19 |
20 | "github.com/FishGoddess/logit"
21 | )
22 |
23 | func main() {
24 | // Default() will return the default logger.
25 | // You can new a logger or just use the default logger.
26 | logger := logit.Default()
27 | logger.Info("nothing carried")
28 |
29 | // Use With() to carry some args in logger.
30 | // All logs output by this logger will carry these args.
31 | logger = logger.With("carry", 666, "who", "me")
32 |
33 | logger.Info("see what are carried")
34 | logger.Error("error carried", "err", io.EOF)
35 |
36 | // Use WithGroup() to group args in logger.
37 | // All logs output by this logger will group args.
38 | logger = logger.WithGroup("xxx")
39 |
40 | logger.Info("what group")
41 | logger.Error("error group", "err", io.EOF)
42 |
43 | // If you want to check if one level can be logged, try this:
44 | if logger.DebugEnabled() {
45 | logger.Debug("debug enabled")
46 | }
47 |
48 | // We provide some old-school logging methods.
49 | // They are using info level by default.
50 | // If you want to change the level, see defaults.LevelPrint.
51 | logger.Printf("printf %s log", "formatted")
52 | logger.Print("print log")
53 | logger.Println("println log")
54 |
55 | // Some useful method:
56 | logger.Sync()
57 | logger.Close()
58 | }
59 |
--------------------------------------------------------------------------------
/_examples/option.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "os"
19 |
20 | "github.com/FishGoddess/logit"
21 | )
22 |
23 | func main() {
24 | // As you can see, NewLogger can use some options to create a logger.
25 | logger := logit.NewLogger(logit.WithDebugLevel())
26 | logger.Debug("debug log")
27 |
28 | // We provide some options for different scenes and all options have prefix "With".
29 | // Change logger level:
30 | logit.WithDebugLevel()
31 | logit.WithInfoLevel()
32 | logit.WithWarnLevel()
33 | logit.WithDebugLevel()
34 |
35 | // Change logger handler:
36 | logit.WithHandler("xxx")
37 | logit.WithTapeHandler()
38 | logit.WithTextHandler()
39 | logit.WithJsonHandler()
40 |
41 | // Change handler writer:
42 | logit.WithWriter(os.Stdout)
43 | logit.WithStdout()
44 | logit.WithStderr()
45 | logit.WithFile("")
46 | logit.WithRotateFile("")
47 |
48 | // Some useful flags:
49 | logit.WithSource()
50 | logit.WithPID()
51 |
52 | // More options can be found in logit package which have prefix "With".
53 | // What's more? We provide a options pack that we think it's useful in production.
54 | opts := logit.ProductionOptions()
55 |
56 | logger = logit.NewLogger(opts...)
57 | defer logger.Close()
58 |
59 | logger.Info("log from production options")
60 | logger.Error("error log from production options")
61 | }
62 |
--------------------------------------------------------------------------------
/_examples/performance_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "io"
19 | "log/slog"
20 | "os"
21 | "path/filepath"
22 | "testing"
23 |
24 | "github.com/FishGoddess/logit"
25 | "github.com/FishGoddess/logit/defaults"
26 | //"github.com/rs/zerolog"
27 | //"github.com/sirupsen/logrus"
28 | //"go.uber.org/zap"
29 | //"go.uber.org/zap/zapcore"
30 | )
31 |
32 | /*
33 | $ go test -v ./_examples/performance_test.go -bench=. -benchtime=1s
34 |
35 | goos: linux
36 | goarch: amd64
37 | cpu: AMD EPYC 7K62 48-Core Processor
38 |
39 | BenchmarkLogitLogger-2 1486184 810 ns/op 0 B/op 0 allocs/op
40 | BenchmarkLogitLoggerTextHandler-2 1000000 1080 ns/op 0 B/op 0 allocs/op
41 | BenchmarkLogitLoggerJsonHandler-2 847864 1393 ns/op 120 B/op 3 allocs/op
42 | BenchmarkLogitLoggerPrint-2 1222302 981 ns/op 48 B/op 1 allocs/op
43 | BenchmarkSlogLoggerTextHandler-2 725522 1629 ns/op 0 B/op 0 allocs/op
44 | BenchmarkSlogLoggerJsonHandler-2 583214 2030 ns/op 120 B/op 3 allocs/op
45 | BenchmarkZeroLogLogger-2 1929276 613 ns/op 0 B/op 0 allocs/op
46 | BenchmarkZapLogger-2 976855 1168 ns/op 216 B/op 2 allocs/op
47 | BenchmarkLogrusLogger-2 231723 4927 ns/op 2080 B/op 32 allocs/op
48 |
49 | BenchmarkLogitFile-2 624774 1935 ns/op 0 B/op 0 allocs/op
50 | BenchmarkLogitFileWithBuffer-2 1378076 873 ns/op 0 B/op 0 allocs/op
51 | BenchmarkLogitFileWithBatch-2 1367479 883 ns/op 0 B/op 0 allocs/op
52 | BenchmarkSlogFile-2 407590 2944 ns/op 0 B/op 0 allocs/op
53 | BenchmarkZeroLogFile-2 634375 1810 ns/op 0 B/op 0 allocs/op
54 | BenchmarkZapFile-2 382790 2641 ns/op 216 B/op 2 allocs/op
55 | BenchmarkLogrusFile-2 174944 6491 ns/op 2080 B/op 32 allocs/op
56 | */
57 |
58 | // go test -v ./_examples/performance_test.go -bench=^BenchmarkLogitLogger$ -benchtime=1s
59 | func BenchmarkLogitLogger(b *testing.B) {
60 | logger := logit.NewLogger(
61 | logit.WithInfoLevel(),
62 | logit.WithTapeHandler(),
63 | logit.WithWriter(io.Discard),
64 | )
65 |
66 | b.ReportAllocs()
67 | b.StartTimer()
68 |
69 | for i := 0; i < b.N; i++ {
70 | logger.Info("info...", "trace", "xxx", "id", 123, "pi", 3.14)
71 | }
72 | }
73 |
74 | // go test -v ./_examples/performance_test.go -bench=^BenchmarkLogitLoggerTextHandler$ -benchtime=1s
75 | func BenchmarkLogitLoggerTextHandler(b *testing.B) {
76 | logger := logit.NewLogger(
77 | logit.WithInfoLevel(),
78 | logit.WithTextHandler(),
79 | logit.WithWriter(io.Discard),
80 | )
81 |
82 | b.ReportAllocs()
83 | b.StartTimer()
84 |
85 | for i := 0; i < b.N; i++ {
86 | logger.Info("info...", "trace", "xxx", "id", 123, "pi", 3.14)
87 | }
88 | }
89 |
90 | // go test -v ./_examples/performance_test.go -bench=^BenchmarkLogitLoggerJsonHandler$ -benchtime=1s
91 | func BenchmarkLogitLoggerJsonHandler(b *testing.B) {
92 | logger := logit.NewLogger(
93 | logit.WithInfoLevel(),
94 | logit.WithJsonHandler(),
95 | logit.WithWriter(io.Discard),
96 | )
97 |
98 | b.ReportAllocs()
99 | b.StartTimer()
100 |
101 | for i := 0; i < b.N; i++ {
102 | logger.Info("info...", "trace", "xxx", "id", 123, "pi", 3.14)
103 | }
104 | }
105 |
106 | // go test -v ./_examples/performance_test.go -bench=^BenchmarkLogitLoggerPrint$ -benchtime=1s
107 | func BenchmarkLogitLoggerPrint(b *testing.B) {
108 | logger := logit.NewLogger(
109 | logit.WithInfoLevel(),
110 | logit.WithTapeHandler(),
111 | logit.WithWriter(io.Discard),
112 | )
113 |
114 | b.ReportAllocs()
115 | b.StartTimer()
116 |
117 | for i := 0; i < b.N; i++ {
118 | logger.Printf("info... %s=%s %s=%d %s=%.3f", "trace", "xxx", "id", 123, "pi", 3.14)
119 | }
120 | }
121 |
122 | // go test -v ./_examples/performance_test.go -bench=^BenchmarkSlogLoggerTextHandler$ -benchtime=1s
123 | func BenchmarkSlogLoggerTextHandler(b *testing.B) {
124 | opts := &slog.HandlerOptions{
125 | Level: slog.LevelInfo,
126 | }
127 |
128 | handler := slog.NewTextHandler(io.Discard, opts)
129 | logger := slog.New(handler)
130 |
131 | b.ReportAllocs()
132 | b.StartTimer()
133 |
134 | for i := 0; i < b.N; i++ {
135 | logger.Info("info...", "trace", "xxx", "id", 123, "pi", 3.14)
136 | }
137 | }
138 |
139 | // go test -v ./_examples/performance_test.go -bench=^BenchmarkSlogLoggerJsonHandler$ -benchtime=1s
140 | func BenchmarkSlogLoggerJsonHandler(b *testing.B) {
141 | opts := &slog.HandlerOptions{
142 | Level: slog.LevelInfo,
143 | }
144 |
145 | handler := slog.NewJSONHandler(io.Discard, opts)
146 | logger := slog.New(handler)
147 |
148 | b.ReportAllocs()
149 | b.StartTimer()
150 |
151 | for i := 0; i < b.N; i++ {
152 | logger.Info("info...", "trace", "xxx", "id", 123, "pi", 3.14)
153 | }
154 | }
155 |
156 | // // go test -v ./_examples/performance_test.go -bench=^BenchmarkZeroLogLogger$ -benchtime=1s
157 | // func BenchmarkZeroLogLogger(b *testing.B) {
158 | // zerolog.TimeFieldFormat = timeFormat
159 | // logger := zerolog.New(io.Discard).Level(zerolog.InfoLevel).With().Timestamp().Logger()
160 | //
161 | // b.ReportAllocs()
162 | // b.StartTimer()
163 | //
164 | // for i := 0; i < b.N; i++ {
165 | // logger.Info().Str("trace", "xxx").Int("id", 123).Float64("pi", 3.14).Msg("info...")
166 | // }
167 | // }
168 | //
169 | // // go test -v ./_examples/performance_test.go -bench=^BenchmarkZapLogger$ -benchtime=1s
170 | // func BenchmarkZapLogger(b *testing.B) {
171 | // config := zap.NewProductionEncoderConfig()
172 | // config.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
173 | // enc.AppendString(t.Format(timeFormat))
174 | // }
175 | //
176 | // encoder := zapcore.NewJSONEncoder(config)
177 | // nopWriteSyncer := zapcore.AddSync(io.Discard)
178 | // core := zapcore.NewCore(encoder, nopWriteSyncer, zapcore.InfoLevel)
179 | //
180 | // logger := zap.New(core)
181 | // defer logger.Sync()
182 | //
183 | // logTask := func() {
184 | // logger.Info("info...", zap.String("trace", "abcxxx"), zap.Int("id", 123), zap.Float64("pi", 3.14))
185 | // }
186 | //
187 | // b.ReportAllocs()
188 | // b.StartTimer()
189 | //
190 | // for i := 0; i < b.N; i++ {
191 | // logTask()
192 | // }
193 | // }
194 | //
195 | // // go test -v ./_examples/performance_test.go -bench=^BenchmarkLogrusLogger$ -benchtime=1s
196 | // func BenchmarkLogrusLogger(b *testing.B) {
197 | // logger := logrus.New()
198 | // logger.SetOutput(io.Discard)
199 | // logger.SetLevel(logrus.InfoLevel)
200 | // logger.SetFormatter(&logrus.JSONFormatter{
201 | // TimestampFormat: timeFormat,
202 | // })
203 | //
204 | // b.ReportAllocs()
205 | // b.StartTimer()
206 | //
207 | // for i := 0; i < b.N; i++ {
208 | // logger.WithFields(map[string]interface{}{"trace": "xxx", "id": 123, "pi": 3.14}).Info("info...")
209 | // }
210 | // }
211 |
212 | // *******************************************************************************
213 |
214 | func openFile(path string) (*os.File, error) {
215 | dir := filepath.Dir(path)
216 | if err := defaults.OpenFileDir(dir, defaults.FileDirMode); err != nil {
217 | return nil, err
218 | }
219 |
220 | return defaults.OpenFile(path, 644)
221 | }
222 |
223 | // go test -v ./_examples/performance_test.go -bench=^BenchmarkLogitFile$ -benchtime=1s
224 | func BenchmarkLogitFile(b *testing.B) {
225 | path := filepath.Join(b.TempDir(), b.Name())
226 |
227 | logger := logit.NewLogger(
228 | logit.WithInfoLevel(),
229 | logit.WithTapeHandler(),
230 | logit.WithFile(path),
231 | )
232 |
233 | b.ReportAllocs()
234 | b.StartTimer()
235 |
236 | for i := 0; i < b.N; i++ {
237 | logger.Info("info...", "trace", "xxx", "id", 123, "pi", 3.14)
238 | }
239 | }
240 |
241 | // go test -v ./_examples/performance_test.go -bench=^BenchmarkLogitFileWithBuffer$ -benchtime=1s
242 | func BenchmarkLogitFileWithBuffer(b *testing.B) {
243 | path := filepath.Join(b.TempDir(), b.Name())
244 |
245 | logger := logit.NewLogger(
246 | logit.WithInfoLevel(),
247 | logit.WithTapeHandler(),
248 | logit.WithFile(path),
249 | logit.WithBuffer(65536),
250 | )
251 |
252 | defer logger.Close()
253 |
254 | b.ReportAllocs()
255 | b.StartTimer()
256 |
257 | for i := 0; i < b.N; i++ {
258 | logger.Info("info...", "trace", "xxx", "id", 123, "pi", 3.14)
259 | }
260 | }
261 |
262 | // go test -v ./_examples/performance_test.go -bench=^BenchmarkLogitFileWithBatch$ -benchtime=1s
263 | func BenchmarkLogitFileWithBatch(b *testing.B) {
264 | path := filepath.Join(b.TempDir(), b.Name())
265 |
266 | logger := logit.NewLogger(
267 | logit.WithInfoLevel(),
268 | logit.WithTapeHandler(),
269 | logit.WithFile(path),
270 | logit.WithBatch(64),
271 | )
272 |
273 | defer logger.Close()
274 |
275 | b.ReportAllocs()
276 | b.StartTimer()
277 |
278 | for i := 0; i < b.N; i++ {
279 | logger.Info("info...", "trace", "xxx", "id", 123, "pi", 3.14)
280 | }
281 | }
282 |
283 | // go test -v ./_examples/performance_test.go -bench=^BenchmarkSlogFile$ -benchtime=1s
284 | func BenchmarkSlogFile(b *testing.B) {
285 | path := filepath.Join(b.TempDir(), b.Name())
286 |
287 | file, _ := openFile(path)
288 | defer file.Close()
289 |
290 | opts := &slog.HandlerOptions{
291 | Level: slog.LevelInfo,
292 | }
293 |
294 | handler := slog.NewTextHandler(file, opts)
295 | logger := slog.New(handler)
296 |
297 | b.ReportAllocs()
298 | b.StartTimer()
299 |
300 | for i := 0; i < b.N; i++ {
301 | logger.Info("info...", "trace", "xxx", "id", 123, "pi", 3.14)
302 | }
303 | }
304 |
305 | // // go test -v ./_examples/performance_test.go -bench=^BenchmarkZeroLogFile$ -benchtime=1s
306 | // func BenchmarkZeroLogFile(b *testing.B) {
307 | // path := filepath.Join(b.TempDir(), b.Name())
308 | //
309 | // file, _ := openFile(path)
310 | // defer file.Close()
311 | //
312 | // zerolog.TimeFieldFormat = timeFormat
313 | // logger := zerolog.New(file).Level(zerolog.InfoLevel).With().Timestamp().Logger()
314 | //
315 | // b.ReportAllocs()
316 | // b.StartTimer()
317 | //
318 | // for i := 0; i < b.N; i++ {
319 | // logger.Info().Str("trace", "xxx").Int("id", 123).Float64("pi", 3.14).Msg("info...")
320 | // }
321 | // }
322 | //
323 | // // go test -v ./_examples/performance_test.go -bench=^BenchmarkZapFile$ -benchtime=1s
324 | // func BenchmarkZapFile(b *testing.B) {
325 | // path := filepath.Join(b.TempDir(), b.Name())
326 | //
327 | // file, _ := openFile(path)
328 | // defer file.Close()
329 | //
330 | // config := zap.NewProductionEncoderConfig()
331 | // config.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
332 | // enc.AppendString(t.Format(timeFormat))
333 | // }
334 | //
335 | // encoder := zapcore.NewJSONEncoder(config)
336 | // writeSyncer := zapcore.AddSync(file)
337 | // core := zapcore.NewCore(encoder, writeSyncer, zapcore.InfoLevel)
338 | //
339 | // logger := zap.New(core)
340 | // defer logger.Sync()
341 | //
342 | // b.ReportAllocs()
343 | // b.StartTimer()
344 | //
345 | // for i := 0; i < b.N; i++ {
346 | // logger.Info("info...", zap.String("trace", "xxx"), zap.Int("id", 123), zap.Float64("pi", 3.14))
347 | // }
348 | // }
349 | //
350 | // // go test -v ./_examples/performance_test.go -bench=^BenchmarkLogrusFile$ -benchtime=1s
351 | // func BenchmarkLogrusFile(b *testing.B) {
352 | // path := filepath.Join(b.TempDir(), b.Name())
353 | //
354 | // file, _ := openFile(path)
355 | // defer file.Close()
356 | //
357 | // logger := logrus.New()
358 | // logger.SetOutput(file)
359 | // logger.SetLevel(logrus.InfoLevel)
360 | // logger.SetFormatter(&logrus.JSONFormatter{
361 | // TimestampFormat: timeFormat,
362 | // })
363 | //
364 | // b.ReportAllocs()
365 | // b.StartTimer()
366 | //
367 | // for i := 0; i < b.N; i++ {
368 | // logger.WithFields(map[string]interface{}{"trace": "xxx", "id": 123, "pi": 3.14}).Info("info...")
369 | // }
370 | // }
371 |
--------------------------------------------------------------------------------
/_examples/writer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "os"
19 |
20 | "github.com/FishGoddess/logit"
21 | )
22 |
23 | func main() {
24 | // A new logger outputs logs to stdout.
25 | logger := logit.NewLogger()
26 | logger.Debug("log to stdout")
27 |
28 | // What if I want to output logs to stderr? Try WithStderr.
29 | logger = logit.NewLogger(logit.WithStderr())
30 | logger.Debug("log to stderr")
31 |
32 | // Also, you can use WithWriter to specify your own writer.
33 | logger = logit.NewLogger(logit.WithWriter(os.Stdout))
34 | logger.Debug("log to writer")
35 |
36 | // How to output logs to a file? Try WithFile and WithRotateFile.
37 | // Rotate file is useful in production, see _examples/file.go.
38 | logger = logit.NewLogger(logit.WithFile("logit.log"))
39 | logger.Debug("log to file")
40 |
41 | logger = logit.NewLogger(logit.WithRotateFile("logit.log"))
42 | logger.Debug("log to rotate file")
43 | }
44 |
--------------------------------------------------------------------------------
/_icons/coverage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/_icons/godoc.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/_icons/license.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logit
16 |
17 | import (
18 | "io"
19 | "log/slog"
20 | "os"
21 | "time"
22 |
23 | "github.com/FishGoddess/logit/handler"
24 | )
25 |
26 | type nilSyncer struct{}
27 |
28 | func (nilSyncer) Sync() error {
29 | return nil
30 | }
31 |
32 | type nilCloser struct{}
33 |
34 | func (nilCloser) Close() error {
35 | return nil
36 | }
37 |
38 | type config struct {
39 | level slog.Level
40 | handler string
41 |
42 | newWriter func() (io.Writer, error)
43 | wrapWriter func(io.Writer) io.Writer
44 |
45 | replaceAttr func(groups []string, attr slog.Attr) slog.Attr
46 |
47 | withSource bool
48 | withPID bool
49 |
50 | syncTimer time.Duration
51 | }
52 |
53 | func newDefaultConfig() *config {
54 | newWriter := func() (io.Writer, error) {
55 | return os.Stdout, nil
56 | }
57 |
58 | conf := &config{
59 | level: slog.LevelDebug,
60 | handler: handler.Tape,
61 | newWriter: newWriter,
62 | wrapWriter: nil,
63 | replaceAttr: nil,
64 | withSource: false,
65 | withPID: false,
66 | syncTimer: 0,
67 | }
68 |
69 | return conf
70 | }
71 |
72 | func (c *config) newSyncer(handler slog.Handler, writer io.Writer) Syncer {
73 | if syncer, ok := handler.(Syncer); ok {
74 | return syncer
75 | }
76 |
77 | if syncer, ok := writer.(Syncer); ok {
78 | return syncer
79 | }
80 |
81 | return nilSyncer{}
82 | }
83 |
84 | func (c *config) newCloser(handler slog.Handler, writer io.Writer) io.Closer {
85 | if closer, ok := handler.(io.Closer); ok {
86 | return closer
87 | }
88 |
89 | if closer, ok := writer.(io.Closer); ok {
90 | return closer
91 | }
92 |
93 | return nilCloser{}
94 | }
95 |
96 | func (c *config) newHandlerOptions() *slog.HandlerOptions {
97 | opts := &slog.HandlerOptions{
98 | Level: c.level,
99 | AddSource: c.withSource,
100 | ReplaceAttr: c.replaceAttr,
101 | }
102 |
103 | return opts
104 | }
105 |
106 | func (c *config) newHandler() (slog.Handler, Syncer, io.Closer, error) {
107 | newHandler, err := handler.Get(c.handler)
108 | if err != nil {
109 | return nil, nil, nil, err
110 | }
111 |
112 | writer, err := c.newWriter()
113 | if err != nil {
114 | return nil, nil, nil, err
115 | }
116 |
117 | if c.wrapWriter != nil {
118 | writer = c.wrapWriter(writer)
119 | }
120 |
121 | opts := c.newHandlerOptions()
122 | handler := newHandler(writer, opts)
123 | syncer := c.newSyncer(handler, writer)
124 | closer := c.newCloser(handler, writer)
125 | return handler, syncer, closer, nil
126 | }
127 |
--------------------------------------------------------------------------------
/config_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logit
16 |
17 | import (
18 | "fmt"
19 | "io"
20 | "log/slog"
21 | "os"
22 | "testing"
23 |
24 | "github.com/FishGoddess/logit/handler"
25 | )
26 |
27 | type testConfigHandler struct {
28 | slog.TextHandler
29 |
30 | w io.Writer
31 | opts slog.HandlerOptions
32 | }
33 |
34 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestConfigNewHandlerOptions$
35 | func TestConfigNewHandlerOptions(t *testing.T) {
36 | replaceAttr := func(groups []string, attr slog.Attr) slog.Attr { return attr }
37 |
38 | conf := &config{
39 | level: slog.LevelWarn,
40 | withSource: true,
41 | replaceAttr: replaceAttr,
42 | }
43 |
44 | opts := conf.newHandlerOptions()
45 |
46 | if opts.Level != conf.level {
47 | t.Fatalf("opts.Level %v != conf.level %v", opts.Level, conf.level)
48 | }
49 |
50 | if opts.AddSource != conf.withSource {
51 | t.Fatalf("opts.AddSource %v != conf.withSource %v", opts.AddSource, conf.withSource)
52 | }
53 |
54 | if fmt.Sprintf("%p", opts.ReplaceAttr) != fmt.Sprintf("%p", conf.replaceAttr) {
55 | t.Fatalf("opts.ReplaceAttr %p != conf.replaceAttr %p", opts.ReplaceAttr, conf.replaceAttr)
56 | }
57 | }
58 |
59 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestConfigNewHandler$
60 | func TestConfigNewHandler(t *testing.T) {
61 | handlerName := t.Name()
62 |
63 | handler.Register(handlerName, func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
64 | return &testConfigHandler{
65 | w: w,
66 | opts: *opts,
67 | }
68 | })
69 |
70 | newWriter := func() (io.Writer, error) { return os.Stderr, nil }
71 | replaceAttr := func(groups []string, attr slog.Attr) slog.Attr { return attr }
72 |
73 | conf := &config{
74 | level: slog.LevelWarn,
75 | handler: handlerName,
76 | newWriter: newWriter,
77 | replaceAttr: replaceAttr,
78 | withSource: true,
79 | }
80 |
81 | handler, syncer, closer, err := conf.newHandler()
82 | if err != nil {
83 | t.Fatal(err)
84 | }
85 |
86 | if syncer == nil {
87 | t.Fatal("syncer is nil")
88 | }
89 |
90 | if closer == nil {
91 | t.Fatal("closer is nil")
92 | }
93 |
94 | tcHandler, ok := handler.(*testConfigHandler)
95 | if !ok {
96 | t.Fatalf("handler type %T is wrong", handler)
97 | }
98 |
99 | if tcHandler.w != os.Stderr {
100 | t.Fatalf("tcHandler.w %p != os.Stderr %p", tcHandler.w, os.Stderr)
101 | }
102 |
103 | if tcHandler.opts.Level != conf.level {
104 | t.Fatalf("tcHandler.opts.Level %v != conf.level %v", tcHandler.opts.Level, conf.level)
105 | }
106 |
107 | if tcHandler.opts.AddSource != conf.withSource {
108 | t.Fatalf("tcHandler.opts.AddSource %v != conf.withSource %v", tcHandler.opts.AddSource, conf.withSource)
109 | }
110 |
111 | if fmt.Sprintf("%p", tcHandler.opts.ReplaceAttr) != fmt.Sprintf("%p", conf.replaceAttr) {
112 | t.Fatalf("tcHandler.opts.ReplaceAttr %p != conf.replaceAttr %p", tcHandler.opts.ReplaceAttr, conf.replaceAttr)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logit
16 |
17 | import (
18 | "context"
19 | )
20 |
21 | type contextKey struct{}
22 |
23 | // NewContext wraps context with logger and returns a new context.
24 | func NewContext(ctx context.Context, logger *Logger) context.Context {
25 | return context.WithValue(ctx, contextKey{}, logger)
26 | }
27 |
28 | // FromContext gets logger from context and returns the default logger if missed.
29 | func FromContext(ctx context.Context) *Logger {
30 | if logger, ok := ctx.Value(contextKey{}).(*Logger); ok {
31 | return logger
32 | }
33 |
34 | return Default()
35 | }
36 |
--------------------------------------------------------------------------------
/context_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logit
16 |
17 | import (
18 | "context"
19 | "testing"
20 | )
21 |
22 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNewContext$
23 | func TestNewContext(t *testing.T) {
24 | logger := NewLogger()
25 | ctx := NewContext(context.Background(), logger)
26 |
27 | value := ctx.Value(contextKey{})
28 | if value == nil {
29 | t.Fatal("value == nil")
30 | }
31 |
32 | contextLogger, ok := value.(*Logger)
33 | if !ok {
34 | t.Fatalf("value type %T is wrong", value)
35 | }
36 |
37 | if contextLogger != logger {
38 | t.Fatalf("contextLogger %+v != logger %+v", contextLogger, logger)
39 | }
40 | }
41 |
42 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestFromContext$
43 | func TestFromContext(t *testing.T) {
44 | ctx := context.Background()
45 | logger := FromContext(ctx)
46 |
47 | if logger == nil {
48 | t.Fatal("logger == nil")
49 | }
50 |
51 | logger = NewLogger()
52 | contextLogger := FromContext(context.WithValue(ctx, contextKey{}, logger))
53 |
54 | if contextLogger != logger {
55 | t.Fatalf("contextLogger %+v != logger %+v", contextLogger, logger)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/default.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logit
16 |
17 | import (
18 | "fmt"
19 | "log/slog"
20 | "sync/atomic"
21 |
22 | "github.com/FishGoddess/logit/defaults"
23 | )
24 |
25 | var defaultLogger atomic.Pointer[*Logger]
26 |
27 | func init() {
28 | SetDefault(NewLogger())
29 | }
30 |
31 | // SetDefault sets logger as the default logger.
32 | func SetDefault(logger *Logger) {
33 | defaultLogger.Store(&logger)
34 | }
35 |
36 | // Default returns the default logger.
37 | func Default() *Logger {
38 | return *defaultLogger.Load()
39 | }
40 |
41 | // Debug logs a log with msg and args in debug level.
42 | func Debug(msg string, args ...any) {
43 | Default().log(slog.LevelDebug, msg, args...)
44 | }
45 |
46 | // Info logs a log with msg and args in info level.
47 | func Info(msg string, args ...any) {
48 | Default().log(slog.LevelInfo, msg, args...)
49 | }
50 |
51 | // Warn logs a log with msg and args in warn level.
52 | func Warn(msg string, args ...any) {
53 | Default().log(slog.LevelWarn, msg, args...)
54 | }
55 |
56 | // Error logs a log with msg and args in error level.
57 | func Error(msg string, args ...any) {
58 | Default().log(slog.LevelError, msg, args...)
59 | }
60 |
61 | // Printf logs a log with format and args in print level.
62 | // It a old-school way to log.
63 | func Printf(format string, args ...interface{}) {
64 | msg := fmt.Sprintf(format, args...)
65 | Default().log(defaults.LevelPrint, msg)
66 | }
67 |
68 | // Print logs a log with args in print level.
69 | // It a old-school way to log.
70 | func Print(args ...interface{}) {
71 | msg := fmt.Sprint(args...)
72 | Default().log(defaults.LevelPrint, msg)
73 | }
74 |
75 | // Println logs a log with args in print level.
76 | // It a old-school way to log.
77 | func Println(args ...interface{}) {
78 | msg := fmt.Sprintln(args...)
79 | Default().log(defaults.LevelPrint, msg)
80 | }
81 |
82 | // Sync syncs the default logger and returns an error if failed.
83 | func Sync() error {
84 | return Default().Sync()
85 | }
86 |
87 | // Close closes the default logger and returns an error if failed.
88 | func Close() error {
89 | return Default().Close()
90 | }
91 |
--------------------------------------------------------------------------------
/default_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logit
16 |
17 | import (
18 | "bytes"
19 | "io"
20 | "log/slog"
21 | "testing"
22 |
23 | "github.com/FishGoddess/logit/handler"
24 | )
25 |
26 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestSetDefault$
27 | func TestSetDefault(t *testing.T) {
28 | logger := NewLogger()
29 | defaultLogger.Store(&logger)
30 |
31 | logger = NewLogger()
32 | SetDefault(logger)
33 |
34 | gotLogger := defaultLogger.Load()
35 | if gotLogger == nil {
36 | t.Fatal("gotLogger == nil")
37 | }
38 |
39 | if *gotLogger != logger {
40 | t.Fatalf("gotLogger %+v != logger %+v", gotLogger, logger)
41 | }
42 | }
43 |
44 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestDefault$
45 | func TestDefault(t *testing.T) {
46 | logger := NewLogger()
47 | defaultLogger.Store(&logger)
48 |
49 | gotLogger := Default()
50 | if gotLogger != logger {
51 | t.Fatalf("gotLogger %+v != logger %+v", gotLogger, logger)
52 | }
53 | }
54 |
55 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestDefaultLogger$
56 | func TestDefaultLogger(t *testing.T) {
57 | handlerName := t.Name()
58 |
59 | newHandler := func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
60 | return slog.NewTextHandler(w, opts)
61 | }
62 |
63 | handler.Register(handlerName, newHandler)
64 |
65 | buffer := bytes.NewBuffer(make([]byte, 0, 1024))
66 | logger := NewLogger(
67 | WithDebugLevel(), WithHandler(handlerName), WithWriter(buffer), WithSource(), WithPID(),
68 | )
69 |
70 | SetDefault(logger)
71 | Debug("debug msg", "key1", 1)
72 | Info("info msg", "key2", 2)
73 | Warn("warn msg", "key3", 3)
74 | Error("error msg", "key4", 4)
75 |
76 | opts := &slog.HandlerOptions{AddSource: true, Level: slog.LevelDebug}
77 | wantBuffer := bytes.NewBuffer(make([]byte, 0, 1024))
78 | slogLogger := slog.New(newHandler(wantBuffer, opts)).With(keyPID, pid)
79 |
80 | slogLogger.Debug("debug msg", "key1", 1)
81 | slogLogger.Info("info msg", "key2", 2)
82 | slogLogger.Warn("warn msg", "key3", 3)
83 | slogLogger.Error("error msg", "key4", 4)
84 |
85 | got := removeTimeAndSource(buffer.String())
86 | want := removeTimeAndSource(wantBuffer.String())
87 |
88 | if got != want {
89 | t.Fatalf("got %s != want %s", got, want)
90 | }
91 | }
92 |
93 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestDefaultLoggerSync$
94 | func TestDefaultLoggerSync(t *testing.T) {
95 | syncer := &testSyncer{
96 | synced: false,
97 | }
98 |
99 | logger := &Logger{
100 | syncer: syncer,
101 | }
102 |
103 | SetDefault(logger)
104 | Sync()
105 |
106 | if !syncer.synced {
107 | t.Fatal("syncer.synced is wrong")
108 | }
109 | }
110 |
111 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestDefaultLoggerClose$
112 | func TestDefaultLoggerClose(t *testing.T) {
113 | syncer := &testSyncer{
114 | synced: false,
115 | }
116 |
117 | closer := &testCloser{
118 | closed: false,
119 | }
120 |
121 | logger := &Logger{
122 | syncer: syncer,
123 | closer: closer,
124 | }
125 |
126 | SetDefault(logger)
127 | Close()
128 |
129 | if !syncer.synced {
130 | t.Fatal("syncer.synced is wrong")
131 | }
132 |
133 | if !closer.closed {
134 | t.Fatal("closer.closed is wrong")
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/defaults/defaults.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package defaults
16 |
17 | import (
18 | "log/slog"
19 | "os"
20 | "time"
21 | )
22 |
23 | var (
24 | // CurrentTime returns the current time with time.Time.
25 | CurrentTime = time.Now
26 |
27 | // HandleError handles an error passed to it.
28 | // You can collect all errors and count them for reporting.
29 | // Notice that this function is called synchronously, so don't do too many things in it.
30 | HandleError = func(label string, err error) {}
31 | )
32 |
33 | var (
34 | // CallerDepth is the depth of caller.
35 | // See runtime.Caller.
36 | CallerDepth = 4
37 |
38 | // LevelPrint is the level used for printing logs.
39 | LevelPrint = slog.LevelInfo
40 | )
41 |
42 | var (
43 | // MinBufferSize is the min buffer size used in bytes.
44 | MinBufferSize = 1 * 1024
45 |
46 | // MaxBufferSize is the max buffer size used in bytes.
47 | MaxBufferSize = 16 * 1024
48 | )
49 |
50 | var (
51 | // FileMode is the permission bits
52 | FileMode os.FileMode = 0644
53 |
54 | // FileDirMode is the permission bits of directory.
55 | FileDirMode os.FileMode = 0755
56 | )
57 |
58 | var (
59 | // OpenFile opens a file of path with given mode.
60 | OpenFile = func(path string, mode os.FileMode) (*os.File, error) {
61 | return os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, mode)
62 | }
63 |
64 | // OpenFileDir opens a dir of path with given mode.
65 | OpenFileDir = func(path string, mode os.FileMode) error {
66 | return os.MkdirAll(path, mode)
67 | }
68 | )
69 |
--------------------------------------------------------------------------------
/extension/config/config.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package config
16 |
17 | import (
18 | "fmt"
19 | "strings"
20 |
21 | "github.com/FishGoddess/logit"
22 | "github.com/FishGoddess/logit/rotate"
23 | )
24 |
25 | type WriterConfig struct {
26 | // Target is where the writer writes logs.
27 | // Values: "stdout", "stderr", or a file path like "./logit.log".
28 | Target string `json:"target" yaml:"target" toml:"target" bson:"target"`
29 |
30 | // FileRotate is log file should split and backup when satisfy some conditions.
31 | // It's useful in production so we recommend you to set it to true.
32 | // Only available when target is a file path.
33 | FileRotate bool `json:"file_rotate" yaml:"file_rotate" toml:"file_rotate" bson:"file_rotate"`
34 |
35 | // FileMaxSize is the max size of a log file.
36 | // If size of data in one output operation is bigger than this value, then file will rotate before writing,
37 | // which means file and its backups may be bigger than this value in size.
38 | // You can use common words like "100MB" or "1GB".
39 | // Only available when rotate is true.
40 | FileMaxSize string `json:"file_max_size" yaml:"file_max_size" toml:"file_max_size" bson:"file_max_size"`
41 |
42 | // FileMaxAge is the time that backups will live.
43 | // All backups reach max age will be removed automatically.
44 | // You can use common words like "7d" or "24h".
45 | // See time.Duration and time.ParseDuration.
46 | // Only available when rotate is true.
47 | FileMaxAge string `json:"file_max_age" yaml:"file_max_age" toml:"file_max_age" bson:"file_max_age"`
48 |
49 | // FileMaxBackups is the max count of file backups.
50 | // Only available when rotate is true.
51 | FileMaxBackups uint32 `json:"file_max_backups" yaml:"file_max_backups" toml:"file_max_backups" bson:"file_max_backups"`
52 |
53 | // BufferSize is the size of a buffer.
54 | // You can use common words like "512B" or "4KB".
55 | // Only available when mode is "buffer".
56 | BufferSize string `json:"buffer_size" yaml:"buffer_size" toml:"buffer_size" bson:"buffer_size"`
57 |
58 | // BatchSize is the size of a batch.
59 | // Only available when mode is "batch".
60 | BatchSize uint64 `json:"batch_size" yaml:"batch_size" toml:"batch_size" bson:"batch_size"`
61 | }
62 |
63 | func (wc *WriterConfig) parseFileOptions() ([]rotate.Option, error) {
64 | opts := make([]rotate.Option, 0, 4)
65 |
66 | if wc.FileMaxSize != "" {
67 | maxSize, err := parseByteSize(wc.FileMaxSize)
68 | if err != nil {
69 | return nil, err
70 | }
71 |
72 | opts = append(opts, rotate.WithMaxSize(maxSize))
73 | }
74 |
75 | if wc.FileMaxAge != "" {
76 | maxAge, err := parseTimeDuration(wc.FileMaxAge)
77 | if err != nil {
78 | return nil, err
79 | }
80 |
81 | opts = append(opts, rotate.WithMaxAge(maxAge))
82 | }
83 |
84 | if wc.FileMaxBackups > 0 {
85 | opts = append(opts, rotate.WithMaxBackups(wc.FileMaxBackups))
86 | }
87 |
88 | return opts, nil
89 | }
90 |
91 | func (wc *WriterConfig) appendTargetOptions(opts []logit.Option) ([]logit.Option, error) {
92 | target := strings.ToLower(wc.Target)
93 |
94 | if target == "" {
95 | return opts, nil
96 | }
97 |
98 | if target == "stdout" {
99 | opts = append(opts, logit.WithStdout())
100 | return opts, nil
101 | }
102 |
103 | if target == "stderr" {
104 | opts = append(opts, logit.WithStderr())
105 | return opts, nil
106 | }
107 |
108 | if !wc.FileRotate {
109 | opts = append(opts, logit.WithFile(wc.Target))
110 | return opts, nil
111 | }
112 |
113 | fileOpts, err := wc.parseFileOptions()
114 | if err != nil {
115 | return nil, err
116 | }
117 |
118 | opts = append(opts, logit.WithRotateFile(wc.Target, fileOpts...))
119 | return opts, nil
120 | }
121 |
122 | func (wc *WriterConfig) appendModeOptions(opts []logit.Option) ([]logit.Option, error) {
123 | if wc.BufferSize != "" {
124 | bufferSize, err := parseByteSize(wc.BufferSize)
125 | if err != nil {
126 | return nil, err
127 | }
128 |
129 | opts = append(opts, logit.WithBuffer(bufferSize))
130 | }
131 |
132 | if wc.BatchSize > 0 {
133 | opts = append(opts, logit.WithBatch(wc.BatchSize))
134 | }
135 |
136 | return opts, nil
137 | }
138 |
139 | // Options parses a writer config and returns a list of options.
140 | // Return an error if parse failed.
141 | func (wc *WriterConfig) Options() (opts []logit.Option, err error) {
142 | opts = make([]logit.Option, 0, 4)
143 |
144 | appendFuncs := []func(opts []logit.Option) ([]logit.Option, error){
145 | wc.appendTargetOptions, wc.appendModeOptions,
146 | }
147 |
148 | for _, append := range appendFuncs {
149 | opts, err = append(opts)
150 | if err != nil {
151 | return nil, err
152 | }
153 | }
154 |
155 | return opts, nil
156 | }
157 |
158 | type Config struct {
159 | // Level is the level of logger.
160 | // Values: debug, info, warn, error.
161 | Level string `json:"level" yaml:"level" toml:"level" bson:"level"`
162 |
163 | // Handler is how the handler handles the logs.
164 | // Values: "tape", "text", "json".
165 | // Also, you can register your handlers to logit, see RegisterHandler.
166 | Handler string `json:"handler" yaml:"handler" toml:"handler" bson:"handler"`
167 |
168 | // Writer is the config of writer.
169 | Writer WriterConfig `json:"writer" yaml:"writer" toml:"writer" bson:"writer"`
170 |
171 | // WithSource adds source to logs if true.
172 | WithSource bool `json:"with_source" yaml:"with_source" toml:"with_source" bson:"with_source"`
173 |
174 | // WithPID adds pid to logs if true.
175 | WithPID bool `json:"with_pid" yaml:"with_pid" toml:"with_pid" bson:"with_pid"`
176 |
177 | // SyncTimer is the timer duration of syncing.
178 | // An empty string means syncing is manual.
179 | // You can use common words like "5m" or "60s".
180 | // See time.Duration and time.ParseDuration.
181 | SyncTimer string `json:"sync_timer" yaml:"sync_timer" toml:"sync_timer" bson:"sync_timer"`
182 | }
183 |
184 | func (c *Config) appendLevelOptions(opts []logit.Option) ([]logit.Option, error) {
185 | if c.Level == "" {
186 | return opts, nil
187 | }
188 |
189 | level := strings.ToLower(c.Level)
190 |
191 | if level == "debug" {
192 | opts = append(opts, logit.WithDebugLevel())
193 | return opts, nil
194 | }
195 |
196 | if level == "info" {
197 | opts = append(opts, logit.WithInfoLevel())
198 | return opts, nil
199 | }
200 |
201 | if level == "warn" {
202 | opts = append(opts, logit.WithWarnLevel())
203 | return opts, nil
204 | }
205 |
206 | if level == "error" {
207 | opts = append(opts, logit.WithErrorLevel())
208 | return opts, nil
209 | }
210 |
211 | return nil, fmt.Errorf("logit: level %s unknown", level)
212 | }
213 |
214 | func (c *Config) appendHandlerOptions(opts []logit.Option) ([]logit.Option, error) {
215 | if c.Handler == "" {
216 | return opts, nil
217 | }
218 |
219 | handler := strings.ToLower(c.Handler)
220 | opts = append(opts, logit.WithHandler(handler))
221 |
222 | return opts, nil
223 | }
224 |
225 | func (c *Config) appendWriterOptions(opts []logit.Option) ([]logit.Option, error) {
226 | writerOpts, err := c.Writer.Options()
227 | if err != nil {
228 | return nil, err
229 | }
230 |
231 | opts = append(opts, writerOpts...)
232 | return opts, nil
233 | }
234 |
235 | func (c *Config) appendFlagOptions(opts []logit.Option) ([]logit.Option, error) {
236 | if c.WithSource {
237 | opts = append(opts, logit.WithSource())
238 | }
239 |
240 | if c.WithPID {
241 | opts = append(opts, logit.WithPID())
242 | }
243 |
244 | return opts, nil
245 | }
246 |
247 | func (c *Config) appendSyncOptions(opts []logit.Option) ([]logit.Option, error) {
248 | if c.SyncTimer == "" {
249 | return opts, nil
250 | }
251 |
252 | syncTimer, err := parseTimeDuration(c.SyncTimer)
253 | if err != nil {
254 | return nil, err
255 | }
256 |
257 | opts = append(opts, logit.WithSyncTimer(syncTimer))
258 | return opts, nil
259 | }
260 |
261 | // Options parses a config and returns a list of options.
262 | // Return an error if parse failed.
263 | func (c *Config) Options() (opts []logit.Option, err error) {
264 | opts = make([]logit.Option, 0, 4)
265 |
266 | appendFuncs := []func(opts []logit.Option) ([]logit.Option, error){
267 | c.appendLevelOptions, c.appendHandlerOptions, c.appendWriterOptions, c.appendFlagOptions, c.appendSyncOptions,
268 | }
269 |
270 | for _, append := range appendFuncs {
271 | opts, err = append(opts)
272 | if err != nil {
273 | return nil, err
274 | }
275 | }
276 |
277 | return opts, nil
278 | }
279 |
--------------------------------------------------------------------------------
/extension/config/config_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package config
16 |
17 | import (
18 | "log/slog"
19 | "os"
20 | "path/filepath"
21 | "strings"
22 | "testing"
23 |
24 | "github.com/FishGoddess/logit"
25 | "github.com/FishGoddess/logit/defaults"
26 | )
27 |
28 | func removeTimeAndSource(str string) string {
29 | str = strings.ReplaceAll(str, "\n", " ")
30 | strs := strings.Split(str, " ")
31 |
32 | var removed strings.Builder
33 | for _, s := range strs {
34 | if strings.HasPrefix(s, slog.TimeKey) {
35 | continue
36 | }
37 |
38 | if strings.HasPrefix(s, slog.SourceKey) {
39 | continue
40 | }
41 |
42 | removed.WriteString(s)
43 | removed.WriteString(" ")
44 | }
45 |
46 | return removed.String()
47 | }
48 |
49 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestConfig$
50 | func TestConfig(t *testing.T) {
51 | logitFile := filepath.Join(t.TempDir(), t.Name()+"_logit.log")
52 | slogFile := filepath.Join(t.TempDir(), t.Name()+"_slog.log")
53 |
54 | conf := Config{
55 | Level: "debug",
56 | Handler: "text",
57 | Writer: WriterConfig{
58 | Target: logitFile,
59 | FileRotate: true,
60 | FileMaxSize: "1GB",
61 | FileMaxAge: "7d",
62 | FileMaxBackups: 30,
63 | BufferSize: "64KB",
64 | BatchSize: 16,
65 | },
66 | WithSource: true,
67 | WithPID: true,
68 | SyncTimer: "1m",
69 | }
70 |
71 | opts, err := conf.Options()
72 | if err != nil {
73 | t.Fatal(err)
74 | }
75 |
76 | logger := logit.NewLogger(opts...)
77 | defer logger.Close()
78 |
79 | logger.Debug("debug msg", "key1", 1)
80 | logger.Info("info msg", "key2", 2)
81 | logger.Warn("warn msg", "key3", 3)
82 | logger.Error("error msg", "key4", 4)
83 | logger.Close()
84 |
85 | file, err := defaults.OpenFile(slogFile, 0644)
86 | if err != nil {
87 | t.Fatal(err)
88 | }
89 |
90 | handlerOpts := &slog.HandlerOptions{AddSource: true, Level: slog.LevelDebug}
91 | slogLogger := slog.New(slog.NewTextHandler(file, handlerOpts)).With("pid", os.Getpid())
92 |
93 | slogLogger.Debug("debug msg", "key1", 1)
94 | slogLogger.Info("info msg", "key2", 2)
95 | slogLogger.Warn("warn msg", "key3", 3)
96 | slogLogger.Error("error msg", "key4", 4)
97 |
98 | gotBytes, err := os.ReadFile(logitFile)
99 | if err != nil {
100 | t.Fatal(err)
101 | }
102 |
103 | wantBytes, err := os.ReadFile(slogFile)
104 | if err != nil {
105 | t.Fatal(err)
106 | }
107 |
108 | got := removeTimeAndSource(string(gotBytes))
109 | want := removeTimeAndSource(string(wantBytes))
110 |
111 | if got != want {
112 | t.Fatalf("got %s != want %s", got, want)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/extension/config/parse.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package config
16 |
17 | import (
18 | "errors"
19 | "strconv"
20 | "strings"
21 | "time"
22 | )
23 |
24 | const (
25 | B = 1 << (10 * iota)
26 | KB
27 | MB
28 | GB
29 |
30 | Day = 24 * time.Hour
31 | )
32 |
33 | func parseByteSizeWithUnit(size string, unit string, unitSize uint64, bitUnit bool) (uint64, error) {
34 | size = strings.TrimSuffix(size, unit)
35 |
36 | n, err := strconv.ParseUint(size, 10, 64)
37 | if err != nil {
38 | return 0, err
39 | }
40 |
41 | if bitUnit {
42 | return n * unitSize / 8, nil
43 | }
44 |
45 | return n * unitSize, nil
46 | }
47 |
48 | // parseByteSize parses byte size in string.
49 | // You should add unit in your size string, like "4MB", "512K", "64".
50 | // The unit will be byte if size string is just a number.
51 | // General units is GB, G, MB, M, KB, K, B and you can see all of them is byte unit.
52 | // If your size string is like "64kb", the result parsed will be 8KB (64kb = 8KB).
53 | func parseByteSize(size string) (uint64, error) {
54 | size = strings.TrimSpace(size)
55 | if size == "" {
56 | return 0, errors.New("logit: parse byte size from an empty string")
57 | }
58 |
59 | bitUnit := false
60 | if strings.HasSuffix(size, "b") {
61 | bitUnit = true
62 | size = strings.TrimSuffix(size, "b")
63 | } else {
64 | size = strings.TrimSuffix(size, "B")
65 | }
66 |
67 | size = strings.ToUpper(size)
68 | if strings.HasSuffix(size, "G") {
69 | return parseByteSizeWithUnit(size, "G", GB, bitUnit)
70 | }
71 |
72 | if strings.HasSuffix(size, "M") {
73 | return parseByteSizeWithUnit(size, "M", MB, bitUnit)
74 | }
75 |
76 | if strings.HasSuffix(size, "K") {
77 | return parseByteSizeWithUnit(size, "K", KB, bitUnit)
78 | }
79 |
80 | return parseByteSizeWithUnit(size, "", B, bitUnit)
81 | }
82 |
83 | func parseTimeDuration(s string) (time.Duration, error) {
84 | if strings.HasSuffix(s, "d") || strings.HasSuffix(s, "D") {
85 | s = strings.TrimSuffix(s, "d")
86 | s = strings.TrimSuffix(s, "D")
87 |
88 | days, err := strconv.ParseInt(s, 10, 64)
89 | if err != nil {
90 | return 0, err
91 | }
92 |
93 | return time.Duration(days) * Day, nil
94 | }
95 |
96 | return time.ParseDuration(s)
97 | }
98 |
--------------------------------------------------------------------------------
/extension/config/parse_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package config
16 |
17 | import (
18 | "testing"
19 | "time"
20 | )
21 |
22 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestParseByteSize$
23 | func TestParseByteSize(t *testing.T) {
24 | tests := []struct {
25 | name string
26 | size string
27 | want uint64
28 | wantErr bool
29 | }{
30 | {name: "64", size: "64", want: 64, wantErr: false},
31 | {name: "128b", size: "128b", want: 16, wantErr: false},
32 | {name: "256B", size: "256B", want: 256, wantErr: false},
33 | {name: "1K", size: "1K", want: 1024, wantErr: false},
34 | {name: "2k", size: "2k", want: 2048, wantErr: false},
35 | {name: "4Kb", size: "4Kb", want: 512, wantErr: false},
36 | {name: "8kb", size: "8kb", want: 1024, wantErr: false},
37 | {name: "4KB", size: "4KB", want: 4096, wantErr: false},
38 | {name: "16kB", size: "16kB", want: 16384, wantErr: false},
39 | {name: "1M", size: "1M", want: 1024 * 1024, wantErr: false},
40 | {name: "2Mb", size: "2Mb", want: 2 * 1024 * 1024 / 8, wantErr: false},
41 | {name: "3MB", size: "3MB", want: 3 * 1024 * 1024, wantErr: false},
42 | {name: "20mB", size: "20mB", want: 20 * 1024 * 1024, wantErr: false},
43 | {name: "24m", size: "24m", want: 24 * 1024 * 1024, wantErr: false},
44 | {name: "48mb", size: "48mb", want: 48 * 1024 * 1024 / 8, wantErr: false},
45 | {name: "1G", size: "1G", want: 1024 * 1024 * 1024, wantErr: false},
46 | {name: "21Gb", size: "21Gb", want: 21 * 1024 * 1024 * 1024 / 8, wantErr: false},
47 | {name: "3GB", size: "3GB", want: 3 * 1024 * 1024 * 1024, wantErr: false},
48 | {name: "20gB", size: "20gB", want: 20 * 1024 * 1024 * 1024, wantErr: false},
49 | {name: "24g", size: "24g", want: 24 * 1024 * 1024 * 1024, wantErr: false},
50 | {name: "48gb", size: "48gb", want: 48 * 1024 * 1024 * 1024 / 8, wantErr: false},
51 | {name: "64x", size: "64x", want: 0, wantErr: true},
52 | {name: "''", size: "", want: 0, wantErr: true},
53 | {name: "M", size: "M", want: 0, wantErr: true},
54 | }
55 |
56 | for _, tt := range tests {
57 | t.Run(tt.name, func(t *testing.T) {
58 | got, err := parseByteSize(tt.size)
59 |
60 | if (err != nil) != tt.wantErr {
61 | t.Errorf("parseByteSize() error = %v, wantErr %v", err, tt.wantErr)
62 | return
63 | }
64 |
65 | if got != tt.want {
66 | t.Errorf("parseByteSize() = %v, want %v", got, tt.want)
67 | }
68 | })
69 | }
70 | }
71 |
72 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestParseTimeDuration$
73 | func TestParseTimeDuration(t *testing.T) {
74 | tests := []struct {
75 | name string
76 | s string
77 | want time.Duration
78 | wantErr bool
79 | }{
80 | {name: "12s", s: "12s", want: 12 * time.Second, wantErr: false},
81 | {name: "3m", s: "3m", want: 3 * time.Minute, wantErr: false},
82 | {name: "24h", s: "24h", want: 24 * time.Hour, wantErr: false},
83 | {name: "24h50m12s", s: "24h50m12s", want: 24*time.Hour + 50*time.Minute + 12*time.Second, wantErr: false},
84 | {name: "7d", s: "7d", want: 7 * 24 * time.Hour, wantErr: false},
85 | {name: "90D", s: "90D", want: 90 * 24 * time.Hour, wantErr: false},
86 | {name: "''", s: "", want: 0, wantErr: true},
87 | {name: "14", s: "14", want: 0, wantErr: true},
88 | }
89 |
90 | for _, tt := range tests {
91 | t.Run(tt.name, func(t *testing.T) {
92 | got, err := parseTimeDuration(tt.s)
93 |
94 | if (err != nil) != tt.wantErr {
95 | t.Errorf("parseTimeDuration() error = %v, wantErr %v", err, tt.wantErr)
96 | return
97 | }
98 |
99 | if got != tt.want {
100 | t.Errorf("parseTimeDuration() = %v, want %v", got, tt.want)
101 | }
102 | })
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/extension/fastclock/fast_clock.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package fastclock
16 |
17 | import (
18 | "sync"
19 | "sync/atomic"
20 | "time"
21 | )
22 |
23 | // fastClock is a clock for getting current time faster.
24 | // It caches current time in nanos and updates it in fixed duration, so it's not a precise way to get current time.
25 | // In fact, we don't recommend you to use it unless you do need a fast way to get current time even the time is "incorrect".
26 | // According to our benchmarks, it does run faster than time.Now:
27 | //
28 | // In my linux server with 2 cores:
29 | // BenchmarkTimeNow-2 19150246 62.26 ns/op 0 B/op 0 allocs/op
30 | // BenchmarkFastClockNow-2 357209233 3.46 ns/op 0 B/op 0 allocs/op
31 | // BenchmarkFastClockNowNanos-2 467461363 2.55 ns/op 0 B/op 0 allocs/op
32 | //
33 | // However, the performance of time.Now is faster enough for 99.9% situations, so we hope you never use it :)
34 | type fastClock struct {
35 | nanos int64
36 | }
37 |
38 | func newClock() *fastClock {
39 | clock := &fastClock{
40 | nanos: time.Now().UnixNano(),
41 | }
42 |
43 | go clock.start()
44 | return clock
45 | }
46 |
47 | func (fc *fastClock) start() {
48 | const duration = 100 * time.Millisecond
49 |
50 | for {
51 | for i := 0; i < 9; i++ {
52 | time.Sleep(duration)
53 | atomic.AddInt64(&fc.nanos, int64(duration))
54 | }
55 |
56 | time.Sleep(duration)
57 | atomic.StoreInt64(&fc.nanos, time.Now().UnixNano())
58 | }
59 | }
60 |
61 | func (fc *fastClock) Nanos() int64 {
62 | return atomic.LoadInt64(&fc.nanos)
63 | }
64 |
65 | var (
66 | clock *fastClock
67 | clockOnce sync.Once
68 | )
69 |
70 | // Now returns the current time from fast clock.
71 | func Now() time.Time {
72 | nanos := NowNanos()
73 | return time.Unix(0, nanos)
74 | }
75 |
76 | // NowNanos returns the current time in nanos from fast clock.
77 | func NowNanos() int64 {
78 | clockOnce.Do(func() {
79 | clock = newClock()
80 | })
81 |
82 | return clock.Nanos()
83 | }
84 |
--------------------------------------------------------------------------------
/extension/fastclock/fast_clock_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package fastclock
16 |
17 | import (
18 | "math"
19 | "math/rand"
20 | "testing"
21 | "time"
22 | )
23 |
24 | // go test -v -run=^$ -bench=^BenchmarkTimeNow$ -benchtime=1s
25 | func BenchmarkTimeNow(b *testing.B) {
26 | b.ReportAllocs()
27 | b.ResetTimer()
28 |
29 | for i := 0; i < b.N; i++ {
30 | time.Now()
31 | }
32 | }
33 |
34 | // go test -v -run=^$ -bench=^BenchmarkFastClockNow$ -benchtime=1s
35 | func BenchmarkFastClockNow(b *testing.B) {
36 | b.ReportAllocs()
37 | b.ResetTimer()
38 |
39 | for i := 0; i < b.N; i++ {
40 | Now()
41 | }
42 | }
43 |
44 | // go test -v -run=^$ -bench=^BenchmarkFastClockNowNanos$ -benchtime=1s
45 | func BenchmarkFastClockNowNanos(b *testing.B) {
46 | b.ReportAllocs()
47 | b.ResetTimer()
48 |
49 | for i := 0; i < b.N; i++ {
50 | NowNanos()
51 | }
52 | }
53 |
54 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNow$
55 | func TestNow(t *testing.T) {
56 | duration := 100 * time.Millisecond
57 |
58 | for i := 0; i < 100; i++ {
59 | got := Now()
60 | gap := time.Since(got)
61 | t.Logf("got: %v, gap: %v", got, gap)
62 |
63 | if math.Abs(float64(gap.Nanoseconds())) > float64(duration)*1.1 {
64 | t.Errorf("now %v is wrong", got)
65 | }
66 |
67 | time.Sleep(time.Duration(rand.Int63n(int64(duration))))
68 | }
69 | }
70 |
71 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNowNanos$
72 | func TestNowNanos(t *testing.T) {
73 | duration := 100 * time.Millisecond
74 |
75 | for i := 0; i < 100; i++ {
76 | gotNanos := NowNanos()
77 | got := time.Unix(0, gotNanos)
78 | gap := time.Since(got)
79 | t.Logf("got: %v, gap: %v", got, gap)
80 |
81 | if math.Abs(float64(gap.Nanoseconds())) > float64(duration)*1.1 {
82 | t.Errorf("now %v is wrong", got)
83 | }
84 |
85 | time.Sleep(time.Duration(rand.Int63n(int64(duration))))
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/FishGoddess/logit
2 |
3 | go 1.21
4 |
--------------------------------------------------------------------------------
/handler/buffer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package handler
16 |
17 | import (
18 | "sync"
19 |
20 | "github.com/FishGoddess/logit/defaults"
21 | )
22 |
23 | var bufferPool = sync.Pool{
24 | New: func() any {
25 | bs := make([]byte, 0, defaults.MinBufferSize)
26 | return &buffer{bs: bs}
27 | },
28 | }
29 |
30 | type buffer struct {
31 | bs []byte
32 | }
33 |
34 | func newBuffer() *buffer {
35 | return bufferPool.Get().(*buffer)
36 | }
37 |
38 | func freeBuffer(buffer *buffer) {
39 | // Return only smaller buffers for reducing peak allocation.
40 | if cap(buffer.bs) <= defaults.MaxBufferSize {
41 | buffer.bs = buffer.bs[:0]
42 | bufferPool.Put(buffer)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/handler/buffer_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package handler
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/FishGoddess/logit/defaults"
21 | )
22 |
23 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBufferPool$
24 | func TestBufferPool(t *testing.T) {
25 | for i := 0; i < 100; i++ {
26 | bs := make([]byte, 0, 2*defaults.MaxBufferSize)
27 | buffer := &buffer{bs: bs}
28 | freeBuffer(buffer)
29 | }
30 |
31 | for i := 0; i < 100; i++ {
32 | buffer := newBuffer()
33 | bs := buffer.bs
34 |
35 | if len(bs) != 0 {
36 | t.Fatalf("len %d of buffer is wrong", len(bs))
37 | }
38 |
39 | if cap(bs) < defaults.MinBufferSize {
40 | t.Fatalf("cap %d of buffer too small", cap(bs))
41 | }
42 |
43 | if cap(bs) > defaults.MaxBufferSize {
44 | t.Fatalf("cap %d of buffer too large", cap(bs))
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/handler/escape.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package handler
16 |
17 | import (
18 | "strconv"
19 | "unicode/utf8"
20 | )
21 |
22 | // needEscapedByte returns if value need to escape.
23 | // The main character should be escaped is ascii less than \u0020.
24 | func needEscapedByte(value byte) bool {
25 | return value < 32
26 | }
27 |
28 | // appendEscapedByte appends escaped value to dst.
29 | // The main character should be escaped is ascii less than \u0020.
30 | func appendEscapedByte(dst []byte, value byte) []byte {
31 | switch value {
32 | case '\b':
33 | return append(dst, '\\', 'b')
34 | case '\f':
35 | return append(dst, '\\', 'f')
36 | case '\n':
37 | return append(dst, '\\', 'n')
38 | case '\r':
39 | return append(dst, '\\', 'r')
40 | case '\t':
41 | return append(dst, '\\', 't')
42 | default:
43 | // ASCii < 16 needs to add \u000 to behind.
44 | if value < 16 {
45 | return strconv.AppendInt(append(dst, '\\', 'u', '0', '0', '0'), int64(value), 16)
46 | }
47 |
48 | // ASCii in [16, 32) needs to add \u00 to behind.
49 | if value < 32 {
50 | return strconv.AppendInt(append(dst, '\\', 'u', '0', '0'), int64(value), 16)
51 | }
52 |
53 | return append(dst, value)
54 | }
55 | }
56 |
57 | // appendEscapedString appends escaped value to dst.
58 | // The main character should be escaped is ascii less than \u0020.
59 | func appendEscapedString(dst []byte, value string) []byte {
60 | start := 0
61 | escaped := false
62 |
63 | for i := 0; i < len(value); i++ {
64 | // Encountered a byte that need escaping, so we appended bytes behinds it and appended it escaped.
65 | if utf8.RuneStart(value[i]) && needEscapedByte(value[i]) {
66 | dst = append(dst, value[start:i]...)
67 | dst = appendEscapedByte(dst, value[i])
68 | start = i + 1
69 | escaped = true
70 | }
71 | }
72 |
73 | if escaped {
74 | return append(dst, value[start:]...)
75 | }
76 |
77 | // There is no need for escaping, just appending like bytes.
78 | return append(dst, value...)
79 | }
80 |
--------------------------------------------------------------------------------
/handler/escape_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package handler
16 |
17 | import "testing"
18 |
19 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestAppendEscapedByte$
20 | func TestAppendEscapedByte(t *testing.T) {
21 | testcases := []byte{'a', '0', '\n', '\t', '\\', '\b', '\f', '\r', '"', 15, 31}
22 | want := `a0\n\t\\b\f\r"\u000f\u001f`
23 |
24 | buffer := make([]byte, 0, 16)
25 | for _, b := range testcases {
26 | buffer = appendEscapedByte(buffer, b)
27 | }
28 |
29 | if string(buffer) != want {
30 | t.Errorf("result %s is wrong", string(buffer))
31 | }
32 | }
33 |
34 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestAppendEscapedString$
35 | func TestAppendEscapedString(t *testing.T) {
36 | testcases := []string{"a0国\n\t\\\b\f\r\"\\b" + string([]byte{15}) + string([]byte{31})}
37 | want := `a0国\n\t\\b\f\r"\b\u000f\u001f`
38 |
39 | buffer := make([]byte, 0, 16)
40 | for _, str := range testcases {
41 | buffer = appendEscapedString(buffer, str)
42 | }
43 |
44 | if string(buffer) != want {
45 | t.Errorf("result %s is wrong", string(buffer))
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/handler/handler.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package handler
16 |
17 | import (
18 | "fmt"
19 | "io"
20 | "log/slog"
21 | "sync"
22 | )
23 |
24 | const (
25 | Tape = "tape"
26 | Text = "text"
27 | Json = "json"
28 | )
29 |
30 | var (
31 | newHandlers = map[string]NewHandlerFunc{
32 | Tape: func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
33 | return NewTapeHandler(w, opts)
34 | },
35 | Text: func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
36 | return slog.NewTextHandler(w, opts)
37 | },
38 | Json: func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
39 | return slog.NewJSONHandler(w, opts)
40 | },
41 | }
42 | )
43 |
44 | var (
45 | newHandlersLock sync.RWMutex
46 | )
47 |
48 | // NewHandlerFunc is a function for creating slog.Handler with w and opts.
49 | type NewHandlerFunc func(w io.Writer, opts *slog.HandlerOptions) slog.Handler
50 |
51 | // Get gets new handler func with name and returns an error if failed.
52 | func Get(name string) (NewHandlerFunc, error) {
53 | newHandlersLock.RLock()
54 | defer newHandlersLock.RUnlock()
55 |
56 | if newHandler, ok := newHandlers[name]; ok {
57 | return newHandler, nil
58 | }
59 |
60 | return nil, fmt.Errorf("logit: handler %s not found", name)
61 | }
62 |
63 | // Register registers newHandler with name.
64 | func Register(name string, newHandler NewHandlerFunc) error {
65 | newHandlersLock.Lock()
66 | defer newHandlersLock.Unlock()
67 |
68 | if _, registered := newHandlers[name]; registered {
69 | return fmt.Errorf("logit: handler %s has been registered", name)
70 | }
71 |
72 | newHandlers[name] = newHandler
73 | return nil
74 | }
75 |
--------------------------------------------------------------------------------
/handler/handler_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package handler
16 |
17 | import (
18 | "fmt"
19 | "io"
20 | "log/slog"
21 | "testing"
22 | )
23 |
24 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestGetHandlerFunc$
25 | func TestGetHandlerFunc(t *testing.T) {
26 | newHandler := func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
27 | return nil
28 | }
29 |
30 | handler := t.Name()
31 | newHandlers[handler] = newHandler
32 |
33 | got, err := Get(handler)
34 | if err != nil {
35 | t.Fatal(err)
36 | }
37 |
38 | if fmt.Sprintf("%p", got) != fmt.Sprintf("%p", newHandler) {
39 | t.Fatalf("got %p is wrong", got)
40 | }
41 | }
42 |
43 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestRegister$
44 | func TestRegister(t *testing.T) {
45 | for name := range newHandlers {
46 | if err := Register(name, nil); err == nil {
47 | t.Fatal("register an existed handler func should be failed")
48 | }
49 | }
50 |
51 | handler := t.Name()
52 | newHandler := func(w io.Writer, opts *slog.HandlerOptions) slog.Handler { return nil }
53 |
54 | if err := Register(handler, newHandler); err != nil {
55 | t.Fatal(err)
56 | }
57 |
58 | got, ok := newHandlers[handler]
59 | if !ok {
60 | t.Fatalf("handler %s not found", handler)
61 | }
62 |
63 | if fmt.Sprintf("%p", got) != fmt.Sprintf("%p", newHandler) {
64 | t.Fatal("newHandler registered is wrong")
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/handler/tape.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apashe License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apashe.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package handler
16 |
17 | import (
18 | "bytes"
19 | "context"
20 | "encoding/json"
21 | "fmt"
22 | "io"
23 | "log/slog"
24 | "runtime"
25 | "strconv"
26 | "sync"
27 | "time"
28 |
29 | "github.com/FishGoddess/logit/defaults"
30 | )
31 |
32 | const (
33 | zero = '0'
34 | lineBreak = '\n'
35 |
36 | dateConnector = '-'
37 | clockConnector = ':'
38 | timeConnector = ' '
39 | timeMillisConnector = '.'
40 |
41 | keyValueConnector = '='
42 | sourceConnector = ':'
43 | groupConnector = "."
44 | )
45 |
46 | var (
47 | attrConnector = []byte(" ¦ ")
48 | )
49 |
50 | var (
51 | emptyAttr = slog.Attr{}
52 | )
53 |
54 | type tapeHandler struct {
55 | w io.Writer
56 | opts slog.HandlerOptions
57 |
58 | group string
59 | groups []string
60 | attrs []slog.Attr
61 |
62 | lock *sync.Mutex
63 | }
64 |
65 | // NewTapeHandler creates a tape handler with w and opts.
66 | // This handler is more readable and faster than slog's handlers.
67 | func NewTapeHandler(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
68 | if opts == nil {
69 | opts = new(slog.HandlerOptions)
70 | }
71 |
72 | if opts.Level == nil {
73 | opts.Level = slog.LevelInfo
74 | }
75 |
76 | handler := &tapeHandler{
77 | w: w,
78 | opts: *opts,
79 | lock: &sync.Mutex{},
80 | }
81 |
82 | return handler
83 | }
84 |
85 | // WithAttrs returns a new handler with attrs.
86 | func (th *tapeHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
87 | if len(attrs) <= 0 {
88 | return th
89 | }
90 |
91 | handler := *th
92 | handler.attrs = append(handler.attrs, attrs...)
93 | return &handler
94 | }
95 |
96 | // WithGroup returns a new handler with group.
97 | func (th *tapeHandler) WithGroup(name string) slog.Handler {
98 | if name == "" {
99 | return th
100 | }
101 |
102 | handler := *th
103 | if handler.group != "" {
104 | handler.group = handler.group + groupConnector
105 | }
106 |
107 | handler.group = handler.group + name
108 | handler.groups = append(handler.groups, name)
109 | return &handler
110 | }
111 |
112 | func (th *tapeHandler) copyGroups(group string) []string {
113 | cap := cap(th.groups)
114 | if group != "" {
115 | cap += 1
116 | }
117 |
118 | groups := make([]string, 0, cap)
119 | groups = append(groups, th.groups...)
120 |
121 | if group != "" {
122 | groups = append(groups, group)
123 | }
124 |
125 | return groups
126 | }
127 |
128 | // Enabled reports whether the logger should ignore logs whose level is lower than passed level.
129 | func (th *tapeHandler) Enabled(ctx context.Context, level slog.Level) bool {
130 | return level >= th.opts.Level.Level()
131 | }
132 |
133 | func (th *tapeHandler) appendKey(bs []byte, group string, key string) []byte {
134 | if key == "" {
135 | return bs
136 | }
137 |
138 | if th.group != "" {
139 | bs = appendEscapedString(bs, th.group)
140 | bs = append(bs, groupConnector...)
141 | }
142 |
143 | if group != "" {
144 | bs = appendEscapedString(bs, group)
145 | bs = append(bs, groupConnector...)
146 | }
147 |
148 | bs = appendEscapedString(bs, key)
149 | bs = append(bs, keyValueConnector)
150 | return bs
151 | }
152 |
153 | func (th *tapeHandler) appendBool(bs []byte, value bool) []byte {
154 | bs = strconv.AppendBool(bs, value)
155 | bs = append(bs, attrConnector...)
156 | return bs
157 | }
158 |
159 | func (th *tapeHandler) appendInt64(bs []byte, value int64) []byte {
160 | bs = strconv.AppendInt(bs, value, 10)
161 | bs = append(bs, attrConnector...)
162 | return bs
163 | }
164 |
165 | func (th *tapeHandler) appendUint64(bs []byte, value uint64) []byte {
166 | bs = strconv.AppendUint(bs, value, 10)
167 | bs = append(bs, attrConnector...)
168 | return bs
169 | }
170 |
171 | func (th *tapeHandler) appendFloat64(bs []byte, value float64) []byte {
172 | bs = strconv.AppendFloat(bs, value, 'f', -1, 64)
173 | bs = append(bs, attrConnector...)
174 | return bs
175 | }
176 |
177 | func (th *tapeHandler) appendString(bs []byte, value string) []byte {
178 | bs = appendEscapedString(bs, value)
179 | bs = append(bs, attrConnector...)
180 | return bs
181 | }
182 |
183 | func (th *tapeHandler) appendDuration(bs []byte, value time.Duration) []byte {
184 | bs = append(bs, value.String()...)
185 | bs = append(bs, attrConnector...)
186 | return bs
187 | }
188 |
189 | func (th *tapeHandler) appendTime(bs []byte, value time.Time) []byte {
190 | // Time format is an usual but expensive operation if using time.AppendFormat,
191 | // so we use a stupid but faster way to format time.
192 | // The result formatted is like "2006-01-02 15:04:05.000".
193 | year, month, day := value.Date()
194 | hour, minute, second := value.Clock()
195 | mircosecond := time.Duration(value.Nanosecond()) / time.Microsecond
196 |
197 | if year < 10 {
198 | bs = append(bs, zero, zero, zero)
199 | } else if year < 100 {
200 | bs = append(bs, zero, zero)
201 | } else if year < 1000 {
202 | bs = append(bs, zero)
203 | }
204 |
205 | bs = strconv.AppendInt(bs, int64(year), 10)
206 | bs = append(bs, dateConnector)
207 |
208 | if month < 10 {
209 | bs = append(bs, zero)
210 | }
211 |
212 | bs = strconv.AppendInt(bs, int64(month), 10)
213 | bs = append(bs, dateConnector)
214 |
215 | if day < 10 {
216 | bs = append(bs, zero)
217 | }
218 |
219 | bs = strconv.AppendInt(bs, int64(day), 10)
220 | bs = append(bs, timeConnector)
221 |
222 | if hour < 10 {
223 | bs = append(bs, zero)
224 | }
225 |
226 | bs = strconv.AppendInt(bs, int64(hour), 10)
227 | bs = append(bs, clockConnector)
228 |
229 | if minute < 10 {
230 | bs = append(bs, zero)
231 | }
232 |
233 | bs = strconv.AppendInt(bs, int64(minute), 10)
234 | bs = append(bs, clockConnector)
235 |
236 | if second < 10 {
237 | bs = append(bs, zero)
238 | }
239 |
240 | bs = strconv.AppendInt(bs, int64(second), 10)
241 | bs = append(bs, timeMillisConnector)
242 |
243 | if mircosecond < 10 {
244 | bs = append(bs, zero, zero, zero, zero, zero)
245 | } else if mircosecond < 100 {
246 | bs = append(bs, zero, zero, zero, zero)
247 | } else if mircosecond < 1000 {
248 | bs = append(bs, zero, zero, zero)
249 | } else if mircosecond < 10000 {
250 | bs = append(bs, zero, zero)
251 | } else if mircosecond < 100000 {
252 | bs = append(bs, zero)
253 | }
254 |
255 | bs = strconv.AppendInt(bs, int64(mircosecond), 10)
256 | bs = append(bs, attrConnector...)
257 | return bs
258 | }
259 |
260 | func (th *tapeHandler) appendAny(bs []byte, value any) []byte {
261 | if err, ok := value.(error); ok {
262 | bs = append(bs, err.Error()...)
263 | bs = append(bs, attrConnector...)
264 | return bs
265 | }
266 |
267 | if stringer, ok := value.(fmt.Stringer); ok {
268 | bs = append(bs, stringer.String()...)
269 | bs = append(bs, attrConnector...)
270 | return bs
271 | }
272 |
273 | marshaled, err := json.Marshal(value)
274 | if err == nil {
275 | bs = append(bs, marshaled...)
276 | bs = append(bs, attrConnector...)
277 | return bs
278 | }
279 |
280 | defaults.HandleError("json.Marshal", err)
281 |
282 | bs = fmt.Appendf(bs, "%+v", value)
283 | bs = append(bs, attrConnector...)
284 | return bs
285 | }
286 |
287 | func (th *tapeHandler) appendAttr(bs []byte, group string, attr slog.Attr) []byte {
288 | kind := attr.Value.Kind()
289 | replaceAttr := th.opts.ReplaceAttr
290 |
291 | if replaceAttr != nil && kind != slog.KindGroup {
292 | groups := th.copyGroups(group)
293 | attr.Value = attr.Value.Resolve()
294 | attr = replaceAttr(groups, attr)
295 | }
296 |
297 | // Resolve the Attr's value before doing anything else.
298 | attr.Value = attr.Value.Resolve()
299 |
300 | if attr.Equal(emptyAttr) {
301 | return bs
302 | }
303 |
304 | if kind == slog.KindGroup {
305 | bs = th.appendAttrs(bs, attr.Key, attr.Value.Group())
306 | return bs
307 | }
308 |
309 | bs = th.appendKey(bs, group, attr.Key)
310 |
311 | switch kind {
312 | case slog.KindBool:
313 | bs = th.appendBool(bs, attr.Value.Bool())
314 | case slog.KindInt64:
315 | bs = th.appendInt64(bs, attr.Value.Int64())
316 | case slog.KindUint64:
317 | bs = th.appendUint64(bs, attr.Value.Uint64())
318 | case slog.KindFloat64:
319 | bs = th.appendFloat64(bs, attr.Value.Float64())
320 | case slog.KindDuration:
321 | bs = th.appendDuration(bs, attr.Value.Duration())
322 | case slog.KindTime:
323 | bs = th.appendTime(bs, attr.Value.Time())
324 | case slog.KindAny:
325 | bs = th.appendAny(bs, attr.Value.Any())
326 | default:
327 | bs = th.appendString(bs, attr.Value.String())
328 | }
329 |
330 | return bs
331 | }
332 |
333 | func (th *tapeHandler) appendAttrs(bs []byte, group string, attrs []slog.Attr) []byte {
334 | for _, attr := range attrs {
335 | bs = th.appendAttr(bs, group, attr)
336 | }
337 |
338 | return bs
339 | }
340 |
341 | func (th *tapeHandler) appendSource(bs []byte, pc uintptr) []byte {
342 | if !th.opts.AddSource || pc == 0 {
343 | return bs
344 | }
345 |
346 | frames := runtime.CallersFrames([]uintptr{pc})
347 | frame, _ := frames.Next()
348 |
349 | bs = append(bs, slog.SourceKey...)
350 | bs = append(bs, keyValueConnector)
351 | bs = appendEscapedString(bs, frame.File)
352 | bs = append(bs, sourceConnector)
353 | bs = strconv.AppendInt(bs, int64(frame.Line), 10)
354 | bs = append(bs, attrConnector...)
355 | return bs
356 | }
357 |
358 | // Handle handles one record and returns an error if failed.
359 | func (th *tapeHandler) Handle(ctx context.Context, record slog.Record) error {
360 | // Setup a buffer for handling record.
361 | buffer := newBuffer()
362 | bs := buffer.bs
363 |
364 | defer func() {
365 | buffer.bs = bs
366 | freeBuffer(buffer)
367 | }()
368 |
369 | // Handling record.
370 | bs = th.appendTime(bs, record.Time)
371 | bs = th.appendString(bs, record.Level.String())
372 | bs = th.appendString(bs, record.Message)
373 | bs = th.appendSource(bs, record.PC)
374 | bs = th.appendAttrs(bs, "", th.attrs)
375 |
376 | if record.NumAttrs() > 0 {
377 | record.Attrs(func(attr slog.Attr) bool {
378 | bs = th.appendAttr(bs, "", attr)
379 | return true
380 | })
381 | }
382 |
383 | bs = bytes.TrimSuffix(bs, attrConnector)
384 | bs = append(bs, lineBreak)
385 |
386 | // Write handled record.
387 | th.lock.Lock()
388 | defer th.lock.Unlock()
389 |
390 | _, err := th.w.Write(bs)
391 | return err
392 | }
393 |
--------------------------------------------------------------------------------
/handler/tape_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package handler
16 |
17 | import (
18 | "bytes"
19 | "errors"
20 | "fmt"
21 | "io"
22 | "log/slog"
23 | "os"
24 | "strings"
25 | "testing"
26 | "testing/slogtest"
27 | "time"
28 | )
29 |
30 | type demo struct {
31 | value string
32 | }
33 |
34 | func (d *demo) String() string {
35 | return d.value
36 | }
37 |
38 | func parseTime(timeValue string) (time.Time, error) {
39 | return time.Parse("2006-01-02 15:04:05.000000", timeValue)
40 | }
41 |
42 | func parseLevel(levelValue string) (slog.Level, error) {
43 | switch levelValue {
44 | case "DEBUG":
45 | return slog.LevelDebug, nil
46 | case "INFO":
47 | return slog.LevelInfo, nil
48 | case "WARN":
49 | return slog.LevelWarn, nil
50 | case "ERROR":
51 | return slog.LevelError, nil
52 | default:
53 | return 0, fmt.Errorf("unknown level %s", levelValue)
54 | }
55 | }
56 |
57 | // 2023-12-20 12:07:42.731993 ¦ INFO ¦ message
58 | // 2023-12-20 12:07:42.732041 ¦ INFO ¦ message ¦ k=v
59 | // 2023-12-20 12:07:42.732045 ¦ INFO ¦ msg ¦ a=b ¦ c=d
60 | // 0001-01-01 00:00:00.000000 ¦ INFO ¦ msg ¦ k=v
61 | // 2023-12-20 12:07:42.732054 ¦ INFO ¦ msg ¦ a=b ¦ k=v
62 | // 2023-12-20 12:07:42.732057 ¦ INFO ¦ msg ¦ a=b ¦ G.c=d ¦ e=f
63 | // 2023-12-20 12:07:42.732059 ¦ INFO ¦ msg ¦ a=b ¦ e=f
64 | // 2023-12-20 12:07:42.732060 ¦ INFO ¦ msg ¦ a=b ¦ c=d ¦ e=f
65 | // 2023-12-20 12:07:42.732062 ¦ INFO ¦ msg ¦ G.a=b
66 | // 2023-12-20 12:07:42.732064 ¦ INFO ¦ msg ¦ G.H.a=b ¦ G.H.c=d ¦ G.H.e=f
67 | // 2023-12-20 12:07:42.732066 ¦ INFO ¦ msg ¦ G.H.a=b ¦ G.H.c=d
68 | // 2023-12-20 12:07:42.732068 ¦ INFO ¦ msg ¦ k=replaced
69 | // 2023-12-20 12:07:42.732071 ¦ INFO ¦ msg ¦ G.a=v1 ¦ G.b=v2
70 | // 2023-12-20 12:07:42.732073 ¦ INFO ¦ msg ¦ k=replaced
71 | // 2023-12-20 12:07:42.732076 ¦ INFO ¦ msg ¦ G.a=v1 ¦ G.b=v2
72 | func parseLog(log string) (map[string]any, error) {
73 | attrs := strings.Split(log, string(attrConnector))
74 | if len(attrs) < 3 {
75 | return nil, errors.New("len(attrs) < 3")
76 | }
77 |
78 | timeValue, levelValue, message := attrs[0], attrs[1], attrs[2]
79 |
80 | t, err := parseTime(timeValue)
81 | if err != nil {
82 | return nil, err
83 | }
84 |
85 | level, err := parseLevel(levelValue)
86 | if err != nil {
87 | return nil, err
88 | }
89 |
90 | result := map[string]any{
91 | slog.LevelKey: level,
92 | slog.MessageKey: message,
93 | }
94 |
95 | if !t.IsZero() {
96 | result[slog.TimeKey] = t
97 | }
98 |
99 | for i := 3; i < len(attrs); i++ {
100 | kv := strings.Split(attrs[i], string(keyValueConnector))
101 | if len(kv) < 2 {
102 | return nil, fmt.Errorf("attr kv len %d < 2", len(kv))
103 | }
104 |
105 | key, value := kv[0], kv[1]
106 | if !strings.Contains(key, groupConnector) {
107 | result[key] = value
108 | continue
109 | }
110 |
111 | index := 0
112 | lastMap := result
113 | groups := strings.Split(key, groupConnector)
114 |
115 | for ; index < len(groups)-1; index++ {
116 | group := groups[index]
117 |
118 | m, ok := lastMap[group]
119 | if !ok {
120 | m = make(map[string]any, 4)
121 | lastMap[group] = m
122 | }
123 |
124 | lastMap = m.(map[string]any)
125 | }
126 |
127 | k := groups[index]
128 | lastMap[k] = value
129 | }
130 |
131 | return result, nil
132 | }
133 |
134 | func parseLogs(logs []string) ([]map[string]any, error) {
135 | result := make([]map[string]any, 0, 16)
136 | for _, log := range logs {
137 | if log == "" {
138 | continue
139 | }
140 |
141 | one, err := parseLog(log)
142 | if err != nil {
143 | return nil, err
144 | }
145 |
146 | result = append(result, one)
147 | }
148 |
149 | return result, nil
150 | }
151 |
152 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestParseLog$
153 | func TestParseLog(t *testing.T) {
154 | log := "2023-12-20 12:07:42.732064 ¦ INFO ¦ msg ¦ G.H.a=b ¦ G.H.c=d ¦ G.H.e=f"
155 |
156 | m, err := parseLog(log)
157 | if err != nil {
158 | t.Fatal(err)
159 | }
160 |
161 | t.Log(m)
162 | }
163 |
164 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestTapeHandler$
165 | func TestTapeHandler(t *testing.T) {
166 | handler := NewTapeHandler(os.Stdout, nil)
167 | //handler := slog.NewTextHandler(os.Stdout, opts)
168 |
169 | logger1 := slog.New(handler).WithGroup("group1").With("id", 123456)
170 | logger1.Info("using console handler 1", slog.Group("log_group1", "k1", 666), "err", io.EOF)
171 |
172 | logger2 := logger1.WithGroup("group2").With("name", "fishgoddess")
173 | logger2.Info("using console handler 2", slog.Group("log_group2", "k2", 888, "k3", "xxx"), "t", time.Date(1977, 10, 24, 25, 35, 17, 999999000, time.Local))
174 |
175 | demo := &demo{"xxx"}
176 | logger1.Info("using console handler 1", slog.Group("log_group1", "k1", 666), "demo", demo, "err", nil)
177 |
178 | buffer := bytes.NewBuffer(make([]byte, 0, 4096))
179 | handler = NewTapeHandler(buffer, nil)
180 |
181 | err := slogtest.TestHandler(handler, func() []map[string]any {
182 | lines := string(buffer.Bytes())
183 | logs := strings.Split(lines, string(lineBreak))
184 |
185 | t.Log(lines)
186 |
187 | kvs, err := parseLogs(logs)
188 | if err != nil {
189 | t.Fatal(err)
190 | }
191 |
192 | return kvs
193 | })
194 |
195 | // Tape handler doesn't always act like a slog handler.
196 | if err != nil {
197 | t.Log(err)
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/logger.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logit
16 |
17 | import (
18 | "context"
19 | "fmt"
20 | "io"
21 | "log/slog"
22 | "os"
23 | "runtime"
24 | "time"
25 |
26 | "github.com/FishGoddess/logit/defaults"
27 | )
28 |
29 | const (
30 | keyBad = "!BADKEY"
31 | keyPID = "pid"
32 | )
33 |
34 | var (
35 | pid = os.Getpid()
36 | )
37 |
38 | // Syncer is an interface that syncs data to somewhere.
39 | type Syncer interface {
40 | Sync() error
41 | }
42 |
43 | // Logger is the entry of logging in logit.
44 | // It has several levels including debug, info, warn and error.
45 | // It's also a syncer or closer if handler is a syncer or closer.
46 | type Logger struct {
47 | handler slog.Handler
48 |
49 | syncer Syncer
50 | closer io.Closer
51 |
52 | withSource bool
53 | withPID bool
54 | }
55 |
56 | // NewLogger creates a logger with given options or panics if failed.
57 | // If you don't want to panic on failing, use NewLoggerGracefully instead.
58 | func NewLogger(opts ...Option) *Logger {
59 | logger, err := NewLoggerGracefully(opts...)
60 | if err != nil {
61 | panic(err)
62 | }
63 |
64 | return logger
65 | }
66 |
67 | // NewLoggerGracefully creates a logger with given options or returns an error if failed.
68 | // It's a more graceful way to create a logger than NewLogger function.
69 | func NewLoggerGracefully(opts ...Option) (*Logger, error) {
70 | conf := newDefaultConfig()
71 |
72 | for _, opt := range opts {
73 | opt.applyTo(conf)
74 | }
75 |
76 | handler, syncer, closer, err := conf.newHandler()
77 | if err != nil {
78 | return nil, err
79 | }
80 |
81 | logger := &Logger{
82 | handler: handler,
83 | syncer: syncer,
84 | closer: closer,
85 | withSource: conf.withSource,
86 | withPID: conf.withPID,
87 | }
88 |
89 | if conf.syncTimer > 0 {
90 | go logger.runSyncTimer(conf.syncTimer)
91 | }
92 |
93 | return logger, nil
94 | }
95 |
96 | func (l *Logger) runSyncTimer(d time.Duration) {
97 | timer := time.NewTimer(d)
98 | defer timer.Stop()
99 |
100 | for {
101 | select {
102 | case <-timer.C:
103 | if err := l.Sync(); err != nil {
104 | defaults.HandleError("logit.Logger.Sync", err)
105 | }
106 | }
107 | }
108 | }
109 |
110 | func (l *Logger) clone() *Logger {
111 | newLogger := *l
112 |
113 | return &newLogger
114 | }
115 |
116 | func (l *Logger) squeezeAttr(args []any) (slog.Attr, []any) {
117 | // len of args must be > 0
118 | switch arg := args[0].(type) {
119 | case slog.Attr:
120 | return arg, args[1:]
121 | case string:
122 | if len(args) <= 1 {
123 | return slog.String(keyBad, arg), nil
124 | }
125 |
126 | return slog.Any(arg, args[1]), args[2:]
127 | default:
128 | return slog.Any(keyBad, arg), args[1:]
129 | }
130 | }
131 |
132 | func (l *Logger) newAttrs(args []any) (attrs []slog.Attr) {
133 | var attr slog.Attr
134 | for len(args) > 0 {
135 | attr, args = l.squeezeAttr(args)
136 | attrs = append(attrs, attr)
137 | }
138 |
139 | return attrs
140 | }
141 |
142 | // With returns a new logger with args.
143 | // All logs from the new logger will carry the given args.
144 | // See slog.Handler.WithAttrs.
145 | func (l *Logger) With(args ...any) *Logger {
146 | if len(args) <= 0 {
147 | return l
148 | }
149 |
150 | attrs := l.newAttrs(args)
151 | if len(attrs) <= 0 {
152 | return l
153 | }
154 |
155 | newLogger := l.clone()
156 | newLogger.handler = l.handler.WithAttrs(attrs)
157 | return newLogger
158 | }
159 |
160 | // WithGroup returns a new logger with group name.
161 | // All logs from the new logger will be grouped by the name.
162 | // See slog.Handler.WithGroup.
163 | func (l *Logger) WithGroup(name string) *Logger {
164 | if name == "" {
165 | return l
166 | }
167 |
168 | newLogger := l.clone()
169 | newLogger.handler = l.handler.WithGroup(name)
170 | return newLogger
171 |
172 | }
173 |
174 | // enabled reports whether the logger should ignore logs whose level is lower.
175 | func (l *Logger) enabled(level slog.Level) bool {
176 | return l.handler.Enabled(context.Background(), level)
177 | }
178 |
179 | // DebugEnabled reports whether the logger should ignore logs whose level is lower than debug.
180 | func (l *Logger) DebugEnabled() bool {
181 | return l.enabled(slog.LevelDebug)
182 | }
183 |
184 | // InfoEnabled reports whether the logger should ignore logs whose level is lower than info.
185 | func (l *Logger) InfoEnabled() bool {
186 | return l.enabled(slog.LevelInfo)
187 | }
188 |
189 | // WarnEnabled reports whether the logger should ignore logs whose level is lower than warn.
190 | func (l *Logger) WarnEnabled() bool {
191 | return l.enabled(slog.LevelWarn)
192 | }
193 |
194 | // ErrorEnabled reports whether the logger should ignore logs whose level is lower than error.
195 | func (l *Logger) ErrorEnabled() bool {
196 | return l.enabled(slog.LevelError)
197 | }
198 |
199 | // PrintEnabled reports whether the logger should ignore logs whose level is lower than print.
200 | func (l *Logger) PrintEnabled() bool {
201 | return l.enabled(defaults.LevelPrint)
202 | }
203 |
204 | func (l *Logger) newRecord(level slog.Level, msg string, args []any) slog.Record {
205 | var pc uintptr
206 |
207 | if l.withSource {
208 | var pcs [1]uintptr
209 | runtime.Callers(defaults.CallerDepth, pcs[:])
210 | pc = pcs[0]
211 | }
212 |
213 | now := defaults.CurrentTime()
214 | record := slog.NewRecord(now, level, msg, pc)
215 |
216 | if l.withPID {
217 | record.AddAttrs(slog.Int(keyPID, pid))
218 | }
219 |
220 | var attr slog.Attr
221 | for len(args) > 0 {
222 | attr, args = l.squeezeAttr(args)
223 | record.AddAttrs(attr)
224 | }
225 |
226 | return record
227 | }
228 |
229 | func (l *Logger) log(level slog.Level, msg string, args ...any) {
230 | if !l.enabled(level) {
231 | return
232 | }
233 |
234 | record := l.newRecord(level, msg, args)
235 |
236 | if err := l.handler.Handle(context.Background(), record); err != nil {
237 | defaults.HandleError("Logger.handler.Handle", err)
238 | }
239 | }
240 |
241 | // Debug logs a log with msg and args in debug level.
242 | func (l *Logger) Debug(msg string, args ...any) {
243 | l.log(slog.LevelDebug, msg, args...)
244 | }
245 |
246 | // Info logs a log with msg and args in info level.
247 | func (l *Logger) Info(msg string, args ...any) {
248 | l.log(slog.LevelInfo, msg, args...)
249 | }
250 |
251 | // Warn logs a log with msg and args in warn level.
252 | func (l *Logger) Warn(msg string, args ...any) {
253 | l.log(slog.LevelWarn, msg, args...)
254 | }
255 |
256 | // Error logs a log with msg and args in error level.
257 | func (l *Logger) Error(msg string, args ...any) {
258 | l.log(slog.LevelError, msg, args...)
259 | }
260 |
261 | // Printf logs a log with format and args in print level.
262 | // It a old-school way to log.
263 | func (l *Logger) Printf(format string, args ...interface{}) {
264 | msg := fmt.Sprintf(format, args...)
265 | l.log(defaults.LevelPrint, msg)
266 | }
267 |
268 | // Print logs a log with args in print level.
269 | // It a old-school way to log.
270 | func (l *Logger) Print(args ...interface{}) {
271 | msg := fmt.Sprint(args...)
272 | l.log(defaults.LevelPrint, msg)
273 | }
274 |
275 | // Println logs a log with args in print level.
276 | // It a old-school way to log.
277 | func (l *Logger) Println(args ...interface{}) {
278 | msg := fmt.Sprintln(args...)
279 | l.log(defaults.LevelPrint, msg)
280 | }
281 |
282 | // Sync syncs the logger and returns an error if failed.
283 | func (l *Logger) Sync() error {
284 | return l.syncer.Sync()
285 | }
286 |
287 | // Close closes the logger and returns an error if failed.
288 | func (l *Logger) Close() error {
289 | if err := l.Sync(); err != nil {
290 | return err
291 | }
292 |
293 | return l.closer.Close()
294 | }
295 |
--------------------------------------------------------------------------------
/logger_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logit
16 |
17 | import (
18 | "bytes"
19 | "io"
20 | "log/slog"
21 | "strings"
22 | "testing"
23 |
24 | "github.com/FishGoddess/logit/handler"
25 | )
26 |
27 | type testLoggerHandler struct {
28 | slog.TextHandler
29 |
30 | w io.Writer
31 | opts slog.HandlerOptions
32 | }
33 |
34 | type testSyncer struct {
35 | synced bool
36 | }
37 |
38 | func (ts *testSyncer) Sync() error {
39 | ts.synced = true
40 | return nil
41 | }
42 |
43 | type testCloser struct {
44 | closed bool
45 | }
46 |
47 | func (tc *testCloser) Close() error {
48 | tc.closed = true
49 | return nil
50 | }
51 |
52 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNewLogger$
53 | func TestNewLogger(t *testing.T) {
54 | handlerName := t.Name()
55 | testHandler := &testLoggerHandler{}
56 |
57 | handler.Register(handlerName, func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
58 | return testHandler
59 | })
60 |
61 | logger := NewLogger(WithHandler(handlerName))
62 | if logger.handler != testHandler {
63 | t.Fatalf("logger.handler %+v != testHandler %+v", logger.handler, testHandler)
64 | }
65 | }
66 |
67 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLoggerClone$
68 | func TestLoggerClone(t *testing.T) {
69 | logger := NewLogger()
70 | newLogger := logger.clone()
71 |
72 | if logger == newLogger {
73 | t.Fatalf("logger %+v == newLogger %+v", logger, newLogger)
74 | }
75 |
76 | if logger.handler != newLogger.handler {
77 | t.Fatalf("logger.handler %+v != newLogger.handler %+v", logger.handler, newLogger.handler)
78 | }
79 | }
80 |
81 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLoggerNewAttrs$
82 | func TestLoggerNewAttrs(t *testing.T) {
83 | logger := NewLogger()
84 |
85 | args := []any{
86 | "key1", 123, "key2", "456", slog.Bool("key3", true), 666, "key4",
87 | }
88 |
89 | attrs := logger.newAttrs(args)
90 | if len(attrs) != 5 {
91 | t.Fatalf("len(attrs) %d != 5", len(attrs))
92 | }
93 |
94 | if attrs[0].String() != "key1=123" {
95 | t.Fatalf("attrs[0] %s is wrong", attrs[0])
96 | }
97 |
98 | if attrs[1].String() != "key2=456" {
99 | t.Fatalf("attrs[1] %s is wrong", attrs[1])
100 | }
101 |
102 | if attrs[2].String() != "key3=true" {
103 | t.Fatalf("attrs[2] %s is wrong", attrs[2])
104 | }
105 |
106 | if attrs[3].String() != keyBad+"=666" {
107 | t.Fatalf("attrs[3] %s is wrong", attrs[3])
108 | }
109 |
110 | if attrs[4].String() != keyBad+"=key4" {
111 | t.Fatalf("attrs[4] %s is wrong", attrs[4])
112 | }
113 | }
114 |
115 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLoggerWith$
116 | func TestLoggerWith(t *testing.T) {
117 | logger := NewLogger()
118 | newLogger := logger.With()
119 |
120 | if logger != newLogger {
121 | t.Fatalf("logger %+v != newLogger %+v", logger, newLogger)
122 | }
123 |
124 | if logger.handler != newLogger.handler {
125 | t.Fatalf("logger.handler %+v != newLogger.handler %+v", logger.handler, newLogger.handler)
126 | }
127 |
128 | newLogger = logger.With("key", 123)
129 |
130 | if logger == newLogger {
131 | t.Fatalf("logger %+v == newLogger %+v", logger, newLogger)
132 | }
133 |
134 | if logger.handler == newLogger.handler {
135 | t.Fatalf("logger.handler %+v == newLogger.handler %+v", logger.handler, newLogger.handler)
136 | }
137 | }
138 |
139 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLoggerWithGroup$
140 | func TestLoggerWithGroup(t *testing.T) {
141 | logger := NewLogger()
142 | newLogger := logger.WithGroup("")
143 |
144 | if logger != newLogger {
145 | t.Fatalf("logger %+v != newLogger %+v", logger, newLogger)
146 | }
147 |
148 | if logger.handler != newLogger.handler {
149 | t.Fatalf("logger.handler %+v != newLogger.handler %+v", logger.handler, newLogger.handler)
150 | }
151 |
152 | newLogger = logger.WithGroup("xxx")
153 |
154 | if logger == newLogger {
155 | t.Fatalf("logger %+v == newLogger %+v", logger, newLogger)
156 | }
157 |
158 | if logger.handler == newLogger.handler {
159 | t.Fatalf("logger.handler %+v == newLogger.handler %+v", logger.handler, newLogger.handler)
160 | }
161 | }
162 |
163 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLoggerEnabled$
164 | func TestLoggerEnabled(t *testing.T) {
165 | logger := NewLogger(WithErrorLevel())
166 |
167 | if logger.enabled(slog.LevelDebug) {
168 | t.Fatal("logger enabled debug")
169 | }
170 |
171 | if logger.enabled(slog.LevelInfo) {
172 | t.Fatal("logger enabled info")
173 | }
174 |
175 | if logger.enabled(slog.LevelWarn) {
176 | t.Fatal("logger enabled warn")
177 | }
178 |
179 | if !logger.enabled(slog.LevelError) {
180 | t.Fatal("logger enabled error")
181 | }
182 | }
183 |
184 | func removeTimeAndSource(str string) string {
185 | str = strings.ReplaceAll(str, "\n", " ")
186 | strs := strings.Split(str, " ")
187 |
188 | var removed strings.Builder
189 | for _, s := range strs {
190 | if strings.HasPrefix(s, slog.TimeKey) {
191 | continue
192 | }
193 |
194 | if strings.HasPrefix(s, slog.SourceKey) {
195 | continue
196 | }
197 |
198 | removed.WriteString(s)
199 | removed.WriteString(" ")
200 | }
201 |
202 | return removed.String()
203 | }
204 |
205 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLogger$
206 | func TestLogger(t *testing.T) {
207 | handlerName := t.Name()
208 |
209 | newHandler := func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
210 | return slog.NewTextHandler(w, opts)
211 | }
212 |
213 | handler.Register(handlerName, newHandler)
214 |
215 | buffer := bytes.NewBuffer(make([]byte, 0, 1024))
216 | logger := NewLogger(
217 | WithDebugLevel(), WithHandler(handlerName), WithWriter(buffer), WithSource(), WithPID(),
218 | )
219 |
220 | logger.Debug("debug msg", "key1", 1)
221 | logger.Info("info msg", "key2", 2)
222 | logger.Warn("warn msg", "key3", 3)
223 | logger.Error("error msg", "key4", 4)
224 |
225 | opts := &slog.HandlerOptions{AddSource: true, Level: slog.LevelDebug}
226 | wantBuffer := bytes.NewBuffer(make([]byte, 0, 1024))
227 | slogLogger := slog.New(newHandler(wantBuffer, opts)).With(keyPID, pid)
228 |
229 | slogLogger.Debug("debug msg", "key1", 1)
230 | slogLogger.Info("info msg", "key2", 2)
231 | slogLogger.Warn("warn msg", "key3", 3)
232 | slogLogger.Error("error msg", "key4", 4)
233 |
234 | got := removeTimeAndSource(buffer.String())
235 | want := removeTimeAndSource(wantBuffer.String())
236 |
237 | if got != want {
238 | t.Fatalf("got %s != want %s", got, want)
239 | }
240 | }
241 |
242 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLoggerSync$
243 | func TestLoggerSync(t *testing.T) {
244 | syncer := &testSyncer{
245 | synced: false,
246 | }
247 |
248 | logger := &Logger{
249 | syncer: syncer,
250 | }
251 |
252 | logger.Sync()
253 |
254 | if !syncer.synced {
255 | t.Fatal("syncer.synced is wrong")
256 | }
257 | }
258 |
259 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLoggerClose$
260 | func TestLoggerClose(t *testing.T) {
261 | syncer := &testSyncer{
262 | synced: false,
263 | }
264 |
265 | closer := &testCloser{
266 | closed: false,
267 | }
268 |
269 | logger := &Logger{
270 | syncer: syncer,
271 | closer: closer,
272 | }
273 |
274 | logger.Close()
275 |
276 | if !syncer.synced {
277 | t.Fatal("syncer.synced is wrong")
278 | }
279 |
280 | if !closer.closed {
281 | t.Fatal("closer.closed is wrong")
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/option.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logit
16 |
17 | import (
18 | "io"
19 | "log/slog"
20 | "os"
21 | "path/filepath"
22 | "time"
23 |
24 | "github.com/FishGoddess/logit/defaults"
25 | "github.com/FishGoddess/logit/handler"
26 | "github.com/FishGoddess/logit/rotate"
27 | "github.com/FishGoddess/logit/writer"
28 | )
29 |
30 | type Option func(conf *config)
31 |
32 | func (o Option) applyTo(conf *config) {
33 | o(conf)
34 | }
35 |
36 | // WithDebugLevel sets debug level to config.
37 | func WithDebugLevel() Option {
38 | return func(conf *config) {
39 | conf.level = slog.LevelDebug
40 | }
41 | }
42 |
43 | // WithInfoLevel sets info level to config.
44 | func WithInfoLevel() Option {
45 | return func(conf *config) {
46 | conf.level = slog.LevelInfo
47 | }
48 | }
49 |
50 | // WithWarnLevel sets warn level to config.
51 | func WithWarnLevel() Option {
52 | return func(conf *config) {
53 | conf.level = slog.LevelWarn
54 | }
55 | }
56 |
57 | // WithErrorLevel sets error level to config.
58 | func WithErrorLevel() Option {
59 | return func(conf *config) {
60 | conf.level = slog.LevelError
61 | }
62 | }
63 |
64 | // WithWriter sets writer to config.
65 | // The writer is for writing logs.
66 | func WithWriter(w io.Writer) Option {
67 | newWriter := func() (io.Writer, error) {
68 | return w, nil
69 | }
70 |
71 | return func(conf *config) {
72 | conf.newWriter = newWriter
73 | }
74 | }
75 |
76 | // WithStdout sets os.Stdout to config.
77 | // All logs will be written to stdout.
78 | func WithStdout() Option {
79 | newWriter := func() (io.Writer, error) {
80 | return os.Stdout, nil
81 | }
82 |
83 | return func(conf *config) {
84 | conf.newWriter = newWriter
85 | }
86 | }
87 |
88 | // WithStderr sets os.Stderr to config.
89 | // All logs will be written to stderr.
90 | func WithStderr() Option {
91 | newWriter := func() (io.Writer, error) {
92 | return os.Stderr, nil
93 | }
94 |
95 | return func(conf *config) {
96 | conf.newWriter = newWriter
97 | }
98 | }
99 |
100 | // WithFile sets file to config.
101 | // All logs will be written to a file in path.
102 | // It will create all directories in path if not existed.
103 | // The permission bits can be specified by defaults package.
104 | // See defaults.FileDirMode and defaults.FileMode.
105 | // If you want to customize the way open dir or file, see defaults.OpenFileDir and defaults.OpenFile.
106 | func WithFile(path string) Option {
107 | newWriter := func() (io.Writer, error) {
108 | dir := filepath.Dir(path)
109 | if err := defaults.OpenFileDir(dir, defaults.FileDirMode); err != nil {
110 | return nil, err
111 | }
112 |
113 | return defaults.OpenFile(path, defaults.FileMode)
114 | }
115 |
116 | return func(conf *config) {
117 | conf.newWriter = newWriter
118 | }
119 | }
120 |
121 | // WithRotateFile sets rotate file to config.
122 | // All logs will be written to a rotate file in path.
123 | // A rotate file is useful in production, see rotate.File.
124 | // The permission bits can be specified by defaults package.
125 | // See defaults.FileDirMode and defaults.FileMode.
126 | // Use rotate.Option to customize your rotate file.
127 | func WithRotateFile(path string, opts ...rotate.Option) Option {
128 | newWriter := func() (io.Writer, error) {
129 | return rotate.New(path, opts...)
130 | }
131 |
132 | return func(conf *config) {
133 | conf.newWriter = newWriter
134 | }
135 | }
136 |
137 | // WithBuffer sets a buffer writer to config.
138 | // You should specify a buffer size in bytes.
139 | // The remained data in buffer may discard if you kill the process without syncing or closing the logger.
140 | func WithBuffer(bufferSize uint64) Option {
141 | wrapWriter := func(w io.Writer) io.Writer {
142 | return writer.Buffer(w, bufferSize)
143 | }
144 |
145 | return func(conf *config) {
146 | conf.wrapWriter = wrapWriter
147 | }
148 | }
149 |
150 | // WithBatch sets a batch writer to config.
151 | // You should specify a batch size in count.
152 | // The remained logs in batch may discard if you kill the process without syncing or closing the logger.
153 | func WithBatch(batchSize uint64) Option {
154 | wrapWriter := func(w io.Writer) io.Writer {
155 | return writer.Batch(w, batchSize)
156 | }
157 |
158 | return func(conf *config) {
159 | conf.wrapWriter = wrapWriter
160 | }
161 | }
162 |
163 | // WithHandler sets handler to config.
164 | // See RegisterHandler.
165 | func WithHandler(handler string) Option {
166 | return func(conf *config) {
167 | conf.handler = handler
168 | }
169 | }
170 |
171 | // WithTapeHandler sets tape handler to config.
172 | func WithTapeHandler() Option {
173 | return func(conf *config) {
174 | conf.handler = handler.Tape
175 | }
176 | }
177 |
178 | // WithTextHandler sets text handler to config.
179 | func WithTextHandler() Option {
180 | return func(conf *config) {
181 | conf.handler = handler.Text
182 | }
183 | }
184 |
185 | // WithJsonHandler sets json handler to config.
186 | func WithJsonHandler() Option {
187 | return func(conf *config) {
188 | conf.handler = handler.Json
189 | }
190 | }
191 |
192 | // WithReplaceAttr sets replaceAttr to config.
193 | func WithReplaceAttr(replaceAttr func(groups []string, attr slog.Attr) slog.Attr) Option {
194 | return func(conf *config) {
195 | conf.replaceAttr = replaceAttr
196 | }
197 | }
198 |
199 | // WithSource sets withSource=true to config.
200 | // All logs will carry their caller information like file and line.
201 | func WithSource() Option {
202 | return func(conf *config) {
203 | conf.withSource = true
204 | }
205 | }
206 |
207 | // WithPID sets withPID=true to config.
208 | // All logs will carry the process id.
209 | func WithPID() Option {
210 | return func(conf *config) {
211 | conf.withPID = true
212 | }
213 | }
214 |
215 | // WithSyncTimer sets a sync timer duration to config.
216 | // It will call Sync() so it depends on the handler used by logger.
217 | func WithSyncTimer(d time.Duration) Option {
218 | return func(conf *config) {
219 | conf.syncTimer = d
220 | }
221 | }
222 |
223 | // ProductionOptions returns some options that we think they are useful in production.
224 | // We recommend you to use them, so we provide this convenient way to create such a logger.
225 | func ProductionOptions() []Option {
226 | opts := []Option{
227 | WithInfoLevel(), WithRotateFile("./logit.log"),
228 | }
229 |
230 | return opts
231 | }
232 |
--------------------------------------------------------------------------------
/option_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logit
16 |
17 | import (
18 | "bytes"
19 | "fmt"
20 | "log/slog"
21 | "os"
22 | "path/filepath"
23 | "testing"
24 | "time"
25 |
26 | "github.com/FishGoddess/logit/handler"
27 | "github.com/FishGoddess/logit/rotate"
28 | "github.com/FishGoddess/logit/writer"
29 | )
30 |
31 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithDebugLevel$
32 | func TestWithDebugLevel(t *testing.T) {
33 | conf := &config{level: slog.LevelError}
34 | WithDebugLevel().applyTo(conf)
35 |
36 | if conf.level != slog.LevelDebug {
37 | t.Fatalf("conf.level %+v != slog.LevelDebug", conf.level)
38 | }
39 | }
40 |
41 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithInfoLevel$
42 | func TestWithInfoLevel(t *testing.T) {
43 | conf := &config{level: slog.LevelError}
44 | WithInfoLevel().applyTo(conf)
45 |
46 | if conf.level != slog.LevelInfo {
47 | t.Fatalf("conf.level %+v != slog.LevelInfo", conf.level)
48 | }
49 | }
50 |
51 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithWarnLevel$
52 | func TestWithWarnLevel(t *testing.T) {
53 | conf := &config{level: slog.LevelError}
54 | WithWarnLevel().applyTo(conf)
55 |
56 | if conf.level != slog.LevelWarn {
57 | t.Fatalf("conf.level %+v != slog.LevelWarn", conf.level)
58 | }
59 | }
60 |
61 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithErrorLevel$
62 | func TestWithErrorLevel(t *testing.T) {
63 | conf := &config{level: slog.LevelDebug}
64 | WithErrorLevel().applyTo(conf)
65 |
66 | if conf.level != slog.LevelError {
67 | t.Fatalf("conf.level %+v != slog.LevelError", conf.level)
68 | }
69 | }
70 |
71 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithWriter$
72 | func TestWithWriter(t *testing.T) {
73 | conf := &config{newWriter: nil}
74 | WithWriter(os.Stdout).applyTo(conf)
75 |
76 | w, err := conf.newWriter()
77 | if err != nil {
78 | t.Fatal(err)
79 | }
80 |
81 | if w != os.Stdout {
82 | t.Fatalf("w %+v != os.Stdout", w)
83 | }
84 | }
85 |
86 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithStdout$
87 | func TestWithStdout(t *testing.T) {
88 | conf := &config{newWriter: nil}
89 | WithStdout().applyTo(conf)
90 |
91 | w, err := conf.newWriter()
92 | if err != nil {
93 | t.Fatal(err)
94 | }
95 |
96 | if w != os.Stdout {
97 | t.Fatalf("w %+v != os.Stdout", w)
98 | }
99 | }
100 |
101 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithStderr$
102 | func TestWithStderr(t *testing.T) {
103 | conf := &config{newWriter: nil}
104 | WithStderr().applyTo(conf)
105 |
106 | w, err := conf.newWriter()
107 | if err != nil {
108 | t.Fatal(err)
109 | }
110 |
111 | if w != os.Stderr {
112 | t.Fatalf("w %+v != os.Stderr", w)
113 | }
114 | }
115 |
116 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithFile$
117 | func TestWithFile(t *testing.T) {
118 | path := filepath.Join(t.TempDir(), t.Name())
119 |
120 | conf := &config{newWriter: nil}
121 | WithFile(path).applyTo(conf)
122 |
123 | w, err := conf.newWriter()
124 | if err != nil {
125 | t.Fatal(err)
126 | }
127 |
128 | file, ok := w.(*os.File)
129 | if !ok {
130 | t.Fatalf("writer type %T is wrong", w)
131 | }
132 |
133 | text := t.Name()
134 | if _, err = w.Write([]byte(text)); err != nil {
135 | t.Fatal(err)
136 | }
137 |
138 | if err = file.Close(); err != nil {
139 | t.Fatal(err)
140 | }
141 |
142 | data, err := os.ReadFile(path)
143 | if err != nil {
144 | t.Fatal(err)
145 | }
146 |
147 | if string(data) != text {
148 | t.Fatalf("string(data) %s != text %s", string(data), text)
149 | }
150 | }
151 |
152 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithRotateFile$
153 | func TestWithRotateFile(t *testing.T) {
154 | path := filepath.Join(t.TempDir(), t.Name())
155 |
156 | conf := &config{newWriter: nil}
157 | WithRotateFile(path).applyTo(conf)
158 |
159 | w, err := conf.newWriter()
160 | if err != nil {
161 | t.Fatal(err)
162 | }
163 |
164 | file, ok := w.(*rotate.File)
165 | if !ok {
166 | t.Fatalf("writer type %T is wrong", w)
167 | }
168 |
169 | text := t.Name()
170 | if _, err = w.Write([]byte(text)); err != nil {
171 | t.Fatal(err)
172 | }
173 |
174 | if err = file.Close(); err != nil {
175 | t.Fatal(err)
176 | }
177 |
178 | data, err := os.ReadFile(path)
179 | if err != nil {
180 | t.Fatal(err)
181 | }
182 |
183 | if string(data) != text {
184 | t.Fatalf("string(data) %s != text %s", string(data), text)
185 | }
186 | }
187 |
188 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithBuffer$
189 | func TestWithBuffer(t *testing.T) {
190 | conf := &config{wrapWriter: nil}
191 | WithBuffer(64).applyTo(conf)
192 |
193 | buffer := bytes.NewBuffer(make([]byte, 0, 128))
194 | w := conf.wrapWriter(buffer)
195 |
196 | ww, ok := w.(*writer.BufferWriter)
197 | if !ok {
198 | t.Fatalf("writer type %T is wrong", w)
199 | }
200 |
201 | text := string(make([]byte, 63))
202 | if _, err := ww.Write([]byte(text)); err != nil {
203 | t.Fatal(err)
204 | }
205 |
206 | data := buffer.Bytes()
207 | if buffer.Len() > 0 {
208 | t.Fatalf("buffer.Len() %d > 0", buffer.Len())
209 | }
210 |
211 | if _, err := ww.Write([]byte(text)); err != nil {
212 | t.Fatal(err)
213 | }
214 |
215 | data = buffer.Bytes()
216 | if string(data) != text {
217 | t.Fatalf("string(data) %s != text %s", string(data), text)
218 | }
219 |
220 | if err := ww.Sync(); err != nil {
221 | t.Fatal(err)
222 | }
223 |
224 | data = buffer.Bytes()
225 | text = text + text
226 |
227 | if string(data) != text {
228 | t.Fatalf("string(data) %s != text %s", string(data), text)
229 | }
230 | }
231 |
232 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithBatch$
233 | func TestWithBatch(t *testing.T) {
234 | conf := &config{wrapWriter: nil}
235 | WithBatch(16).applyTo(conf)
236 |
237 | buffer := bytes.NewBuffer(make([]byte, 0, 256))
238 | w := conf.wrapWriter(buffer)
239 |
240 | bw, ok := w.(*writer.BatchWriter)
241 | if !ok {
242 | t.Fatalf("writer type %T is wrong", w)
243 | }
244 |
245 | text := string(make([]byte, 4))
246 | for i := 0; i < 15; i++ {
247 | if _, err := bw.Write([]byte(text)); err != nil {
248 | t.Fatal(err)
249 | }
250 | }
251 |
252 | data := buffer.Bytes()
253 | if buffer.Len() > 0 {
254 | t.Fatalf("buffer.Len() %d > 0", buffer.Len())
255 | }
256 |
257 | for i := 0; i < 15; i++ {
258 | if _, err := bw.Write([]byte(text)); err != nil {
259 | t.Fatal(err)
260 | }
261 | }
262 |
263 | data = buffer.Bytes()
264 | want := ""
265 | for i := 0; i < 16; i++ {
266 | want = want + text
267 | }
268 |
269 | if string(data) != want {
270 | t.Fatalf("string(data) %s != want %s", string(data), want)
271 | }
272 |
273 | if err := bw.Sync(); err != nil {
274 | t.Fatal(err)
275 | }
276 |
277 | data = buffer.Bytes()
278 | for i := 0; i < 14; i++ {
279 | want = want + text
280 | }
281 |
282 | if string(data) != want {
283 | t.Fatalf("string(data) %s != want %s", string(data), want)
284 | }
285 | }
286 |
287 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithHandler$
288 | func TestWithHandler(t *testing.T) {
289 | handler := t.Name()
290 | conf := &config{handler: ""}
291 | WithHandler(handler).applyTo(conf)
292 |
293 | if conf.handler != handler {
294 | t.Fatal("conf.handler is wrong")
295 | }
296 | }
297 |
298 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithTapeHandler$
299 | func TestWithTapeHandler(t *testing.T) {
300 | conf := &config{handler: ""}
301 | WithTapeHandler().applyTo(conf)
302 |
303 | if conf.handler != handler.Tape {
304 | t.Fatal("conf.handler is wrong")
305 | }
306 | }
307 |
308 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithTextHandler$
309 | func TestWithTextHandler(t *testing.T) {
310 | conf := &config{handler: ""}
311 | WithTextHandler().applyTo(conf)
312 |
313 | if conf.handler != handler.Text {
314 | t.Fatal("conf.handler is wrong")
315 | }
316 | }
317 |
318 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithJsonHandler$
319 | func TestWithJsonHandler(t *testing.T) {
320 | conf := &config{handler: ""}
321 | WithJsonHandler().applyTo(conf)
322 |
323 | if conf.handler != handler.Json {
324 | t.Fatal("conf.handler is wrong")
325 | }
326 | }
327 |
328 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithReplaceAttr$
329 | func TestWithReplaceAttr(t *testing.T) {
330 | replaceAttr := func(groups []string, attr slog.Attr) slog.Attr { return slog.Attr{} }
331 |
332 | conf := &config{replaceAttr: nil}
333 | WithReplaceAttr(replaceAttr).applyTo(conf)
334 |
335 | if fmt.Sprintf("%p", conf.replaceAttr) != fmt.Sprintf("%p", replaceAttr) {
336 | t.Fatal("conf.replaceAttr is wrong")
337 | }
338 | }
339 |
340 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithSource$
341 | func TestWithSource(t *testing.T) {
342 | conf := &config{withSource: false}
343 | WithSource().applyTo(conf)
344 |
345 | if !conf.withSource {
346 | t.Fatal("conf.withSource is wrong")
347 | }
348 | }
349 |
350 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithPID$
351 | func TestWithPID(t *testing.T) {
352 | conf := &config{withPID: false}
353 | WithPID().applyTo(conf)
354 |
355 | if !conf.withPID {
356 | t.Fatal("conf.withPID is wrong")
357 | }
358 | }
359 |
360 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithSyncTimer$
361 | func TestWithSyncTimer(t *testing.T) {
362 | conf := &config{syncTimer: 0}
363 | WithSyncTimer(time.Minute).applyTo(conf)
364 |
365 | if conf.syncTimer != time.Minute {
366 | t.Fatal("conf.syncTimer is wrong")
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/rotate/backup.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package rotate
16 |
17 | import (
18 | "path/filepath"
19 | "sort"
20 | "strconv"
21 | "time"
22 |
23 | "github.com/FishGoddess/logit/defaults"
24 | )
25 |
26 | const (
27 | backupSeparator = "."
28 | )
29 |
30 | type backup struct {
31 | path string
32 | t time.Time
33 | }
34 |
35 | func (b backup) before(t time.Time) bool {
36 | return b.t.Before(t)
37 | }
38 |
39 | func sortBackups(backups []backup) {
40 | sort.Slice(backups, func(i, j int) bool {
41 | return backups[i].before(backups[j].t)
42 | })
43 | }
44 |
45 | func backupPrefixAndExt(path string) (prefix string, ext string) {
46 | ext = filepath.Ext(path)
47 | prefix = path[:len(path)-len(ext)] + backupSeparator
48 |
49 | return prefix, ext
50 | }
51 |
52 | func backupPath(path string, timeFormat string) string {
53 | now := defaults.CurrentTime()
54 | name, ext := backupPrefixAndExt(path)
55 |
56 | if timeFormat != "" {
57 | return name + now.Format(timeFormat) + ext
58 | }
59 |
60 | return name + strconv.FormatInt(now.Unix(), 10) + ext
61 | }
62 |
63 | func parseBackupTime(filename string, prefix string, ext string, timeFormat string) (time.Time, error) {
64 | ts := filename[len(prefix) : len(filename)-len(ext)]
65 |
66 | if timeFormat != "" {
67 | return time.Parse(timeFormat, ts)
68 | }
69 |
70 | seconds, err := strconv.ParseInt(ts, 10, 64)
71 | if err != nil {
72 | return time.Time{}, err
73 | }
74 |
75 | return time.Unix(seconds, 0), nil
76 | }
77 |
--------------------------------------------------------------------------------
/rotate/backup_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package rotate
16 |
17 | import (
18 | "testing"
19 | "time"
20 |
21 | "github.com/FishGoddess/logit/defaults"
22 | )
23 |
24 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBackupBefore$
25 | func TestBackupBefore(t *testing.T) {
26 | b := backup{t: time.Unix(2, 0)}
27 |
28 | if b.before(time.Unix(1, 0)) {
29 | t.Fatalf("b.before(time.Unix(1, 0)) returns false")
30 | }
31 |
32 | if b.before(time.Unix(2, 0)) {
33 | t.Fatalf("b.before(time.Unix(2, 0)) returns false")
34 | }
35 | }
36 |
37 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestSortBackups$
38 | func TestSortBackups(t *testing.T) {
39 | backups := []backup{
40 | {t: time.Unix(2, 0)},
41 | {t: time.Unix(1, 0)},
42 | {t: time.Unix(4, 0)},
43 | {t: time.Unix(0, 0)},
44 | {t: time.Unix(3, 0)},
45 | }
46 |
47 | sortBackups(backups)
48 |
49 | for i, backup := range backups {
50 | if backup.t.Unix() != int64(i) {
51 | t.Fatalf("backup.t.Unix() %d != int64(i) %d", backup.t.Unix(), int64(i))
52 | }
53 | }
54 | }
55 |
56 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBackupPrefixAndExt$
57 | func TestBackupPrefixAndExt(t *testing.T) {
58 | prefix, ext := backupPrefixAndExt("test.log")
59 |
60 | want := "test" + backupSeparator
61 | if prefix != want {
62 | t.Fatalf("prefix %s != want %s", prefix, want)
63 | }
64 |
65 | want = ".log"
66 | if ext != want {
67 | t.Fatalf("ext %s != want %s", ext, want)
68 | }
69 | }
70 |
71 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBackupPath$
72 | func TestBackupPath(t *testing.T) {
73 | defaults.CurrentTime = func() time.Time {
74 | return time.Unix(1, 0).In(time.UTC)
75 | }
76 |
77 | path := backupPath("test.log", "20060102150405")
78 | want := "test.19700101000001.log"
79 | if path != want {
80 | t.Fatalf("path %s != want %s", path, want)
81 | }
82 | }
83 |
84 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestParseBackupTime$
85 | func TestParseBackupTime(t *testing.T) {
86 | defaults.CurrentTime = func() time.Time {
87 | return time.Now().In(time.UTC)
88 | }
89 |
90 | filename := "test.19700101000001.log"
91 | prefix := "test."
92 | ext := ".log"
93 | timeFormat := "20060102150405"
94 |
95 | backupTime, err := parseBackupTime(filename, prefix, ext, timeFormat)
96 | if err != nil {
97 | t.Fatal(err)
98 | }
99 |
100 | if backupTime.Unix() != 1 {
101 | t.Fatalf("backupTime.Unix() %d != 1", backupTime.Unix())
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/rotate/config.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package rotate
16 |
17 | import (
18 | "time"
19 | )
20 |
21 | const (
22 | MB = 1024 * 1024
23 | Day = 24 * time.Hour
24 | )
25 |
26 | type config struct {
27 | // path is the path of file.
28 | path string
29 |
30 | // timeFormat is the time format of backup path.
31 | timeFormat string
32 |
33 | // maxSize is the max size of file.
34 | // If size of data in one write is bigger than maxSize, then file will rotate and write it,
35 | // which means file and its backup may be bigger than maxSize in size.
36 | maxSize uint64
37 |
38 | // maxAge is how long that backup will live.
39 | // All backups reached maxAge will be cleaned automatically.
40 | maxAge time.Duration
41 |
42 | // maxBackups is the max count of backups.
43 | maxBackups uint32
44 | }
45 |
46 | func newDefaultConfig(path string) *config {
47 | return &config{
48 | path: path,
49 | timeFormat: "20060102150405",
50 | maxSize: 128 * MB,
51 | maxAge: 60 * Day,
52 | maxBackups: 90,
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/rotate/config_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package rotate
16 |
17 | import (
18 | "testing"
19 | )
20 |
21 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNewDefaultConfig$
22 | func TestNewDefaultConfig(t *testing.T) {
23 | conf := newDefaultConfig("xxx")
24 |
25 | want := &config{
26 | path: "xxx",
27 | timeFormat: "20060102150405",
28 | maxSize: 128 * MB,
29 | maxAge: 60 * Day,
30 | maxBackups: 90,
31 | }
32 |
33 | if *conf != *want {
34 | t.Fatalf("conf %+v != want %+v", conf, want)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/rotate/file.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package rotate
16 |
17 | import (
18 | "fmt"
19 | "os"
20 | "path/filepath"
21 | "strings"
22 | "sync"
23 |
24 | "github.com/FishGoddess/logit/defaults"
25 | )
26 |
27 | // File is a file which supports rotating automatically.
28 | // It has max size and file will rotate if size exceeds max size.
29 | // It has max age and max backups, so rotated files will be cleaned which is beneficial to space.
30 | type File struct {
31 | conf *config
32 |
33 | file *os.File
34 | size uint64
35 | ch chan struct{}
36 |
37 | lock sync.Mutex
38 | }
39 |
40 | // New returns a new rotate file.
41 | func New(path string, opts ...Option) (*File, error) {
42 | f := newFile(path, opts)
43 |
44 | if err := f.mkdir(); err != nil {
45 | return nil, err
46 | }
47 |
48 | if err := f.openNewFile(); err != nil {
49 | return nil, err
50 | }
51 |
52 | go f.runCleanTask()
53 | return f, nil
54 | }
55 |
56 | // newFile creates a new file.
57 | func newFile(path string, opts []Option) *File {
58 | conf := newDefaultConfig(path)
59 |
60 | for _, opt := range opts {
61 | opt.applyTo(conf)
62 | }
63 |
64 | f := &File{
65 | conf: conf,
66 | ch: make(chan struct{}, 1),
67 | }
68 |
69 | return f
70 | }
71 |
72 | func (f *File) mkdir() error {
73 | dir := filepath.Dir(f.conf.path)
74 |
75 | return defaults.OpenFileDir(dir, defaults.FileDirMode)
76 | }
77 |
78 | func (f *File) open() (*os.File, error) {
79 | return defaults.OpenFile(f.conf.path, defaults.FileMode)
80 | }
81 |
82 | func (f *File) listBackups() ([]backup, error) {
83 | dir := filepath.Dir(f.conf.path)
84 |
85 | files, err := os.ReadDir(dir)
86 | if err != nil {
87 | return nil, err
88 | }
89 |
90 | baseName := filepath.Base(f.conf.path)
91 | prefix, ext := backupPrefixAndExt(baseName)
92 |
93 | var backups []backup
94 | for _, file := range files {
95 | if file.IsDir() {
96 | continue
97 | }
98 |
99 | filename := file.Name()
100 | if filename == baseName {
101 | continue
102 | }
103 |
104 | notBackup := !strings.HasPrefix(filename, prefix) || !strings.HasSuffix(filename, ext)
105 | if notBackup {
106 | continue
107 | }
108 |
109 | t, err := parseBackupTime(filename, prefix, ext, f.conf.timeFormat)
110 | if err != nil {
111 | defaults.HandleError("rotate.parseBackupTime", err)
112 | continue
113 | }
114 |
115 | backups = append(backups, backup{
116 | path: filepath.Join(dir, filename),
117 | t: t,
118 | })
119 | }
120 |
121 | sortBackups(backups)
122 | return backups, nil
123 | }
124 |
125 | func (f *File) removeStaleBackups(backups []backup) {
126 | staleBackups := make(map[string]struct{}, 16)
127 |
128 | if f.conf.maxBackups > 0 {
129 | exceeds := len(backups) - int(f.conf.maxBackups)
130 | for i := 0; i < exceeds; i++ {
131 | staleBackups[backups[i].path] = struct{}{}
132 | }
133 | }
134 |
135 | if f.conf.maxAge > 0 {
136 | deadline := defaults.CurrentTime().Add(-f.conf.maxAge)
137 |
138 | for _, backup := range backups {
139 | if !backup.before(deadline) {
140 | break
141 | }
142 |
143 | staleBackups[backup.path] = struct{}{}
144 | }
145 | }
146 |
147 | for backup := range staleBackups {
148 | os.Remove(backup)
149 | }
150 | }
151 |
152 | func (f *File) clean() {
153 | backups, err := f.listBackups()
154 | if err != nil {
155 | return
156 | }
157 |
158 | f.removeStaleBackups(backups)
159 | }
160 |
161 | func (f *File) runCleanTask() {
162 | for range f.ch {
163 | f.clean()
164 | }
165 | }
166 |
167 | func (f *File) triggerCleanTask() {
168 | select {
169 | case f.ch <- struct{}{}:
170 | default:
171 | }
172 | }
173 |
174 | func (f *File) openNewFile() error {
175 | file, err := f.open()
176 | if err != nil {
177 | return err
178 | }
179 |
180 | info, err := file.Stat()
181 | if err != nil {
182 | return err
183 | }
184 |
185 | f.file = file
186 | f.size = uint64(info.Size())
187 | return nil
188 | }
189 |
190 | func (f *File) nextBackupPath() (string, error) {
191 | backupPath := backupPath(f.conf.path, f.conf.timeFormat)
192 |
193 | _, err := os.Stat(backupPath)
194 | if os.IsNotExist(err) {
195 | return backupPath, nil
196 | }
197 |
198 | if err != nil {
199 | return "", err
200 | }
201 |
202 | // Backup path conflicted...
203 | err = fmt.Errorf("logit: rotate.file wants a backup path %s but conflicted", backupPath)
204 | return "", err
205 | }
206 |
207 | func (f *File) closeOldFile() (err error) {
208 | backupPath, err := f.nextBackupPath()
209 | if err != nil {
210 | return err
211 | }
212 |
213 | fileClosed := false
214 |
215 | defer func() {
216 | if err != nil && fileClosed {
217 | f.openNewFile() // Reopen closed file.
218 | }
219 | }()
220 |
221 | if err = f.file.Close(); err != nil {
222 | return err
223 | }
224 |
225 | fileClosed = true
226 | err = os.Rename(f.conf.path, backupPath)
227 | return err
228 | }
229 |
230 | func (f *File) rotate() error {
231 | if err := f.closeOldFile(); err != nil {
232 | return err
233 | }
234 |
235 | if err := f.openNewFile(); err != nil {
236 | return err
237 | }
238 |
239 | f.triggerCleanTask()
240 | return nil
241 | }
242 |
243 | // Write writes len(p) bytes from p to the underlying data stream.
244 | func (f *File) Write(p []byte) (n int, err error) {
245 | f.lock.Lock()
246 | defer f.lock.Unlock()
247 |
248 | writeSize := uint64(len(p))
249 | if f.size+writeSize > f.conf.maxSize {
250 | // Ignore rotating error so this p won't be discarded.
251 | if rotateErr := f.rotate(); rotateErr != nil {
252 | defaults.HandleError("rotate.File.rotate", rotateErr)
253 | }
254 | }
255 |
256 | n, err = f.file.Write(p)
257 | f.size += uint64(n)
258 | return n, err
259 | }
260 |
261 | // Sync syncs data to the underlying io device.
262 | func (f *File) Sync() error {
263 | f.lock.Lock()
264 | defer f.lock.Unlock()
265 |
266 | return f.file.Sync()
267 | }
268 |
269 | // Close closes file and returns an error if failed.
270 | func (f *File) Close() error {
271 | f.lock.Lock()
272 | defer f.lock.Unlock()
273 |
274 | if err := f.file.Sync(); err != nil {
275 | return err
276 | }
277 |
278 | close(f.ch)
279 | return f.file.Close()
280 | }
281 |
--------------------------------------------------------------------------------
/rotate/file_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package rotate
16 |
17 | import (
18 | "os"
19 | "path/filepath"
20 | "testing"
21 | "time"
22 |
23 | "github.com/FishGoddess/logit/defaults"
24 | )
25 |
26 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNew$
27 | func TestNew(t *testing.T) {
28 | path := filepath.Join(t.TempDir(), t.Name())
29 |
30 | f, err := New(path)
31 | if err != nil {
32 | t.Fatal(err)
33 | }
34 |
35 | defer f.Close()
36 |
37 | data := []byte("水不要鱼")
38 | n, err := f.Write(data)
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 |
43 | if n != len(data) {
44 | t.Fatalf("n %d != len(data) %d", n, len(data))
45 | }
46 |
47 | read, err := os.ReadFile(path)
48 | if err != nil {
49 | t.Fatalf("ReadFile error: %v", err)
50 | }
51 |
52 | if string(read) != string(data) {
53 | t.Fatalf("string(read) %s != string(data) %s", read, data)
54 | }
55 | }
56 |
57 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNewExisting$
58 | func TestNewExisting(t *testing.T) {
59 | path := filepath.Join(t.TempDir(), t.Name())
60 |
61 | err := os.WriteFile(path, []byte("水不要鱼"), 0644)
62 | if err != nil {
63 | t.Fatal(err)
64 | }
65 |
66 | f, err := New(path)
67 | if err != nil {
68 | t.Fatal(err)
69 | }
70 |
71 | defer f.Close()
72 |
73 | data := []byte("FishGoddess")
74 | n, err := f.Write(data)
75 | if err != nil {
76 | t.Fatal(err)
77 | }
78 |
79 | if n != len(data) {
80 | t.Fatalf("n %d != len(data) %d", n, len(data))
81 | }
82 |
83 | read, err := os.ReadFile(path)
84 | if err != nil {
85 | t.Fatalf("ReadFile error: %v", err)
86 | }
87 |
88 | want := "水不要鱼FishGoddess"
89 | if string(read) != want {
90 | t.Fatalf("string(read) %s != want %s", read, want)
91 | }
92 | }
93 |
94 | func countFiles(dir string) int {
95 | files, _ := os.ReadDir(dir)
96 | return len(files)
97 | }
98 |
99 | // go test -v -cover -count=1 -run=^TestFileRotate$
100 | func TestFileRotate(t *testing.T) {
101 | second := int64(0)
102 | defaults.CurrentTime = func() time.Time {
103 | second++
104 | return time.Unix(second, 0)
105 | }
106 |
107 | dir := filepath.Join(t.TempDir(), t.Name())
108 | if err := os.RemoveAll(dir); err != nil {
109 | t.Fatal(err)
110 | }
111 |
112 | path := filepath.Join(dir, "test.log")
113 |
114 | f, err := New(path)
115 | if err != nil {
116 | t.Fatal(err)
117 | }
118 |
119 | f.conf.maxSize = 4
120 | defer f.Close()
121 |
122 | data := []byte("test")
123 | n, err := f.Write(data)
124 | if err != nil {
125 | t.Fatal(err)
126 | }
127 |
128 | if n != len(data) {
129 | t.Fatalf("n %d != len(data) %d", n, len(data))
130 | }
131 |
132 | read, err := os.ReadFile(path)
133 | if err != nil {
134 | t.Fatal(err)
135 | }
136 |
137 | if string(read) != string(data) {
138 | t.Fatalf("string(read) %s != string(data) %s", read, data)
139 | }
140 |
141 | count := countFiles(dir)
142 | if count != 1 {
143 | t.Fatalf("count %d != 1", count)
144 | }
145 |
146 | data = []byte("burst")
147 | n, err = f.Write(data)
148 | if err != nil {
149 | t.Fatal(err)
150 | }
151 |
152 | if n != len(data) {
153 | t.Fatalf("n %d != len(data) %d", n, len(data))
154 | }
155 |
156 | data = []byte("!!!")
157 | n, err = f.Write(data)
158 | if err != nil {
159 | t.Fatal(err)
160 | }
161 |
162 | if n != len(data) {
163 | t.Fatalf("n %d != len(data) %d", n, len(data))
164 | }
165 |
166 | read, err = os.ReadFile(path)
167 | if err != nil {
168 | t.Fatal(err)
169 | }
170 |
171 | if string(read) != "!!!" {
172 | t.Fatalf("string(read) %s != '!!!'", read)
173 | }
174 |
175 | count = countFiles(dir)
176 | if count != 3 {
177 | t.Fatalf("count %d != 3", count)
178 | }
179 |
180 | second = 3
181 | defaults.CurrentTime = func() time.Time {
182 | second--
183 | return time.Unix(second, 0)
184 | }
185 |
186 | var bs []byte
187 | for second > 1 {
188 | backup := backupPath(path, f.conf.timeFormat)
189 | if bs, err = os.ReadFile(backup); err != nil {
190 | t.Fatal(err)
191 | }
192 |
193 | read = append(read, bs...)
194 | }
195 |
196 | if string(read) != "!!!bursttest" {
197 | t.Fatalf("string(read) %s != '!!!bursttest'", read)
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/rotate/option.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package rotate
16 |
17 | import (
18 | "time"
19 | )
20 |
21 | type Option func(conf *config)
22 |
23 | func (o Option) applyTo(conf *config) {
24 | o(conf)
25 | }
26 |
27 | // WithMaxSize sets max size to config.
28 | func WithMaxSize(size uint64) Option {
29 | return func(conf *config) {
30 | conf.maxSize = size
31 | }
32 | }
33 |
34 | // WithMaxAge sets max age to config.
35 | func WithMaxAge(age time.Duration) Option {
36 | return func(conf *config) {
37 | conf.maxAge = age
38 | }
39 | }
40 |
41 | // WithMaxBackups sets max backups to config.
42 | func WithMaxBackups(backups uint32) Option {
43 | return func(conf *config) {
44 | conf.maxBackups = backups
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/rotate/option_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package rotate
16 |
17 | import (
18 | "testing"
19 | "time"
20 | )
21 |
22 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithMaxSize$
23 | func TestWithMaxSize(t *testing.T) {
24 | conf := newDefaultConfig(t.Name())
25 | conf.maxSize = 0
26 |
27 | WithMaxSize(4 * 1024).applyTo(conf)
28 |
29 | want := newDefaultConfig(t.Name())
30 | want.maxSize = 4 * 1024
31 |
32 | if *conf != *want {
33 | t.Fatalf("conf %+v != want %+v", conf, want)
34 | }
35 | }
36 |
37 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithMaxAge$
38 | func TestWithMaxAge(t *testing.T) {
39 | conf := newDefaultConfig(t.Name())
40 | conf.maxAge = 0
41 |
42 | WithMaxAge(24 * time.Hour).applyTo(conf)
43 |
44 | want := newDefaultConfig(t.Name())
45 | want.maxAge = 24 * time.Hour
46 |
47 | if *conf != *want {
48 | t.Fatalf("conf %+v != want %+v", conf, want)
49 | }
50 | }
51 |
52 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithMaxBackups$
53 | func TestWithMaxBackups(t *testing.T) {
54 | conf := newDefaultConfig(t.Name())
55 | conf.maxBackups = 0
56 |
57 | WithMaxBackups(30).applyTo(conf)
58 |
59 | want := newDefaultConfig(t.Name())
60 | want.maxBackups = 30
61 |
62 | if *conf != *want {
63 | t.Fatalf("conf %+v != want %+v", conf, want)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/writer/batch.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package writer
15 |
16 | import (
17 | "bytes"
18 | "fmt"
19 | "io"
20 | "sync"
21 | )
22 |
23 | const (
24 | // minBatchSize is the min size of batch.
25 | // A panic will happen if batch size is smaller than it.
26 | minBatchSize = 1
27 | )
28 |
29 | // BatchWriter is a writer having a buffer inside to reduce times of writing underlying writer.
30 | type BatchWriter struct {
31 | // writer is the underlying writer to write data.
32 | writer io.Writer
33 |
34 | // maxBatches is the max size of batch.
35 | maxBatches uint64
36 |
37 | // currentBatches is the current size of batch.
38 | currentBatches uint64
39 |
40 | // buffer is for keeping data together and writing them one time.
41 | // Data won't be written to underlying writer if batch size is less than max batch size,
42 | // so you can pre-write them by Sync() if you want.
43 | buffer *bytes.Buffer
44 |
45 | lock sync.Mutex
46 | }
47 |
48 | // Batch returns a new batch writer of writer with specified batchSize.
49 | // Notice that batchSize must be larger than minBatchSize or a panic will happen.
50 | // See minBatchSize.
51 | func Batch(writer io.Writer, batchSize uint64) *BatchWriter {
52 | if batchSize < minBatchSize {
53 | panic(fmt.Errorf("logit: batchSize %d < minBatchSize %d", batchSize, minBatchSize))
54 | }
55 |
56 | if bw, ok := writer.(*BatchWriter); ok {
57 | return bw
58 | }
59 |
60 | bw := &BatchWriter{
61 | writer: writer,
62 | maxBatches: batchSize,
63 | currentBatches: 0,
64 | buffer: bytes.NewBuffer(make([]byte, 0, defaultBufferSize)),
65 | }
66 |
67 | return bw
68 | }
69 |
70 | // Write writes p to buffer and syncs data to underlying writer first if it needs.
71 | func (bw *BatchWriter) Write(p []byte) (n int, err error) {
72 | bw.lock.Lock()
73 | defer bw.lock.Unlock()
74 |
75 | if bw.currentBatches >= bw.maxBatches {
76 | bw.sync()
77 | bw.currentBatches = 0
78 | }
79 |
80 | bw.currentBatches++
81 | return bw.buffer.Write(p)
82 | }
83 |
84 | func (bw *BatchWriter) sync() error {
85 | _, err := bw.buffer.WriteTo(bw.writer)
86 | return err
87 | }
88 |
89 | // Sync writes data in buffer to underlying writer if buffer has data.
90 | // It's safe in concurrency.
91 | func (bw *BatchWriter) Sync() error {
92 | bw.lock.Lock()
93 | defer bw.lock.Unlock()
94 |
95 | if bw.buffer.Len() > 0 {
96 | return bw.sync()
97 | }
98 |
99 | return nil
100 | }
101 |
102 | func (bw *BatchWriter) close() error {
103 | if closer, ok := bw.writer.(io.Closer); ok && notStdoutAndStderr(bw.writer) {
104 | return closer.Close()
105 | }
106 |
107 | return nil
108 | }
109 |
110 | // Close syncs data and closes underlying writer if writer implements io.Closer.
111 | func (bw *BatchWriter) Close() error {
112 | bw.lock.Lock()
113 | defer bw.lock.Unlock()
114 |
115 | if err := bw.sync(); err != nil {
116 | return err
117 | }
118 |
119 | return bw.close()
120 | }
121 |
--------------------------------------------------------------------------------
/writer/batch_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package writer
16 |
17 | import (
18 | "bytes"
19 | "os"
20 | "testing"
21 | "time"
22 | )
23 |
24 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBatch$
25 | func TestBatch(t *testing.T) {
26 | writer := Batch(os.Stdout, 16)
27 |
28 | if writer == nil {
29 | t.Fatal("writer == nil")
30 | }
31 |
32 | if writer.maxBatches != 16 {
33 | t.Fatalf("writer.maxBatches %d is wrong", writer.maxBatches)
34 | }
35 |
36 | newWriter := Batch(writer, 64)
37 | if newWriter != writer {
38 | t.Fatal("newWriter is wrong")
39 | }
40 | }
41 |
42 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBatchWriter$
43 | func TestBatchWriter(t *testing.T) {
44 | buffer := bytes.NewBuffer(make([]byte, 0, 4096))
45 |
46 | writer := Batch(buffer, 10)
47 | defer writer.Close()
48 |
49 | writer.Write([]byte("abc"))
50 | writer.Sync()
51 |
52 | if buffer.String() != "abc" {
53 | t.Fatalf("writing abc but found %s in buffer", buffer.String())
54 | }
55 |
56 | writer.Write([]byte("123"))
57 | writer.Write([]byte(".!?"))
58 | writer.Write([]byte("+-*/"))
59 | writer.Close()
60 | time.Sleep(time.Second)
61 |
62 | if buffer.String() != "abc123.!?+-*/" {
63 | t.Fatalf("writing abc123.!?+-*/ but found %s in buffer", buffer.String())
64 | }
65 | }
66 |
67 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBatchWriterSize$
68 | func TestBatchWriterSize(t *testing.T) {
69 | buffer := bytes.NewBuffer(make([]byte, 0, 4096))
70 |
71 | writer := Batch(buffer, 10)
72 | defer writer.Close()
73 |
74 | for i := 0; i < 10; i++ {
75 | writer.Write([]byte("1"))
76 | }
77 |
78 | if buffer.String() != "" {
79 | t.Fatalf("buffer.String() %s != ''", buffer.String())
80 | }
81 |
82 | writer.Sync()
83 | if buffer.String() != "1111111111" {
84 | t.Fatalf("buffer.String() %s != '1111111111'", buffer.String())
85 | }
86 |
87 | buffer.Reset()
88 | for i := 0; i < 12; i++ {
89 | writer.Write([]byte("1"))
90 | }
91 |
92 | if buffer.String() != "1111111111" {
93 | t.Fatalf("buffer.String() %s != '1111111111'", buffer.String())
94 | }
95 |
96 | writer.Sync()
97 | if buffer.String() != "111111111111" {
98 | t.Fatalf("buffer.String() %s != '111111111111'", buffer.String())
99 | }
100 | }
101 |
102 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBatchWriterClose$
103 | func TestBatchWriterClose(t *testing.T) {
104 | writer := Batch(os.Stdout, 10)
105 | for i := 0; i < 10; i++ {
106 | if err := writer.Close(); err != nil {
107 | t.Fatal(err)
108 | }
109 | }
110 |
111 | writer = Batch(os.Stderr, 10)
112 | for i := 0; i < 10; i++ {
113 | if err := writer.Close(); err != nil {
114 | t.Fatal(err)
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/writer/buffer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package writer
16 |
17 | import (
18 | "bytes"
19 | "fmt"
20 | "io"
21 | "sync"
22 | )
23 |
24 | const (
25 | // minBufferSize is the min size of buffer.
26 | // A panic will happen if buffer size is smaller than it.
27 | minBufferSize = 4
28 | )
29 |
30 | // BufferWriter is a writer having a buffer inside to reduce times of writing underlying writer.
31 | type BufferWriter struct {
32 | // writer is the underlying writer to write data.
33 | writer io.Writer
34 |
35 | // maxBufferSize is the max size of buffer.
36 | maxBufferSize uint64
37 |
38 | // buffer is for keeping data together and writing them one time.
39 | // Data won't be written to underlying writer if buffer isn't full,
40 | // so you can pre-write them by Sync() if you need.
41 | buffer *bytes.Buffer
42 |
43 | lock sync.Mutex
44 | }
45 |
46 | // Buffer returns a new buffer writer of writer with specified bufferSize.
47 | // Notice that bufferSize must be larger than minBufferSize or a panic will happen.
48 | // See minBufferSize.
49 | func Buffer(writer io.Writer, bufferSize uint64) *BufferWriter {
50 | if bufferSize < minBufferSize {
51 | panic(fmt.Errorf("bufferSize %d < minBufferSize %d", bufferSize, minBufferSize))
52 | }
53 |
54 | if bw, ok := writer.(*BufferWriter); ok {
55 | return bw
56 | }
57 |
58 | bw := &BufferWriter{
59 | writer: writer,
60 | maxBufferSize: bufferSize,
61 | buffer: bytes.NewBuffer(make([]byte, 0, bufferSize)),
62 | }
63 |
64 | return bw
65 | }
66 |
67 | // Write writes p to buffer and syncs data to underlying writer first if it needs.
68 | func (bw *BufferWriter) Write(p []byte) (n int, err error) {
69 | bw.lock.Lock()
70 | defer bw.lock.Unlock()
71 |
72 | // This p is too large, so we write it directly to avoid copying.
73 | needBufferSize := len(p)
74 | tooLarge := uint64(needBufferSize) >= bw.maxBufferSize
75 | if tooLarge {
76 | bw.sync()
77 | return bw.writer.Write(p)
78 | }
79 |
80 | // The remaining buffer is not enough, sync data to write this p.
81 | needBufferSize = bw.buffer.Len() + len(p)
82 | notEnough := uint64(needBufferSize) >= bw.maxBufferSize
83 | if notEnough {
84 | bw.sync()
85 | }
86 |
87 | return bw.buffer.Write(p)
88 | }
89 |
90 | func (bw *BufferWriter) sync() error {
91 | _, err := bw.buffer.WriteTo(bw.writer)
92 | return err
93 | }
94 |
95 | // Sync writes data in buffer to underlying writer if buffer has data.
96 | // It's safe in concurrency.
97 | func (bw *BufferWriter) Sync() error {
98 | bw.lock.Lock()
99 | defer bw.lock.Unlock()
100 |
101 | if bw.buffer.Len() > 0 {
102 | return bw.sync()
103 | }
104 |
105 | return nil
106 | }
107 |
108 | func (bw *BufferWriter) close() error {
109 | if closer, ok := bw.writer.(io.Closer); ok && notStdoutAndStderr(bw.writer) {
110 | return closer.Close()
111 | }
112 |
113 | return nil
114 | }
115 |
116 | // Close syncs data and closes underlying writer if writer implements io.Closer.
117 | func (bw *BufferWriter) Close() error {
118 | bw.lock.Lock()
119 | defer bw.lock.Unlock()
120 |
121 | if err := bw.sync(); err != nil {
122 | return err
123 | }
124 |
125 | return bw.close()
126 | }
127 |
--------------------------------------------------------------------------------
/writer/buffer_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package writer
16 |
17 | import (
18 | "bytes"
19 | "os"
20 | "testing"
21 | "time"
22 | )
23 |
24 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBuffer$
25 | func TestBuffer(t *testing.T) {
26 | writer := Buffer(os.Stdout, 1024)
27 |
28 | if writer == nil {
29 | t.Fatal("writer == nil")
30 | }
31 |
32 | if writer.maxBufferSize != 1024 {
33 | t.Fatalf("writer.maxBufferSize %d is wrong", writer.maxBufferSize)
34 | }
35 |
36 | newWriter := Buffer(writer, 4096)
37 | if newWriter != writer {
38 | t.Fatal("newWriter is wrong")
39 | }
40 | }
41 |
42 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBufferWriter$
43 | func TestBufferWriter(t *testing.T) {
44 | buffer := bytes.NewBuffer(make([]byte, 0, 4096))
45 |
46 | writer := Buffer(buffer, 65536)
47 | defer writer.Close()
48 |
49 | writer.Write([]byte("abc"))
50 | writer.Sync()
51 |
52 | if buffer.String() != "abc" {
53 | t.Fatalf("writing abc but found %s in buffer", buffer.String())
54 | }
55 |
56 | writer.Write([]byte("123"))
57 | writer.Write([]byte(".!?"))
58 | writer.Write([]byte("+-*/"))
59 | writer.Close()
60 | time.Sleep(time.Second)
61 |
62 | if buffer.String() != "abc123.!?+-*/" {
63 | t.Fatalf("writing abc123.!?+-*/ but found %s in buffer", buffer.String())
64 | }
65 | }
66 |
67 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestBufferWriterClose$
68 | func TestBufferWriterClose(t *testing.T) {
69 | writer := Buffer(os.Stdout, 4096)
70 | for i := 0; i < 10; i++ {
71 | if err := writer.Close(); err != nil {
72 | t.Fatal(err)
73 | }
74 | }
75 |
76 | writer = Buffer(os.Stderr, 4096)
77 | for i := 0; i < 10; i++ {
78 | if err := writer.Close(); err != nil {
79 | t.Fatal(err)
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/writer/writer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package writer
16 |
17 | import (
18 | "io"
19 | "os"
20 | )
21 |
22 | const (
23 | defaultBufferSize = 64 * 1024 // 64KB
24 | )
25 |
26 | // notStdoutAndStderr returns true if w isn't stdout and stderr.
27 | func notStdoutAndStderr(w io.Writer) bool {
28 | return w != os.Stdout && w != os.Stderr
29 | }
30 |
--------------------------------------------------------------------------------
/writer/writer_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 FishGoddess. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package writer
16 |
17 | import (
18 | "os"
19 | "testing"
20 | )
21 |
22 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNotStdoutAndStderr$
23 | func TestNotStdoutAndStderr(t *testing.T) {
24 | if notStdoutAndStderr(os.Stdout) {
25 | t.Fatal("notStdoutAndStderr(os.Stdout) returns true")
26 | }
27 |
28 | if notStdoutAndStderr(os.Stderr) {
29 | t.Fatal("notStdoutAndStderr(os.Stderr) returns true")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------