├── .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 | [![Go Doc](_icons/godoc.svg)](https://pkg.go.dev/github.com/FishGoddess/logit) 4 | [![License](_icons/license.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) 5 | [![Coverage](_icons/coverage.svg)](_icons/coverage.svg) 6 | ![Test](https://github.com/FishGoddess/logit/actions/workflows/test.yml/badge.svg) 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 | [![Star History Chart](https://api.star-history.com/svg?repos=fishgoddess/logit&type=Date)](https://star-history.com/#fishgoddess/logit&Date) 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📝 logit 2 | 3 | [![Go Doc](_icons/godoc.svg)](https://pkg.go.dev/github.com/FishGoddess/logit) 4 | [![License](_icons/license.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) 5 | [![Coverage](_icons/coverage.svg)](_icons/coverage.svg) 6 | ![Test](https://github.com/FishGoddess/logit/actions/workflows/test.yml/badge.svg) 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 | [![Star History Chart](https://api.star-history.com/svg?repos=fishgoddess/logit&type=Date)](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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | coverage 12 | coverage 13 | 82% 14 | 82% 15 | 16 | -------------------------------------------------------------------------------- /_icons/godoc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | godoc 12 | godoc 13 | reference 14 | reference 15 | 16 | -------------------------------------------------------------------------------- /_icons/license.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | license 16 | license 17 | Apache 18 | Apache 19 | 20 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------