├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── changelog.yml ├── dependabot.yml ├── revive.toml └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── README.zh-CN.md ├── _example ├── bench_loglibs.md ├── bench_loglibs_test.go ├── demos │ ├── demo1.go │ ├── simple.go │ └── slog_all_level.go ├── diff-with-zap-zerolog.md ├── go.mod ├── handler │ ├── grouped.go │ └── multi_file.go ├── images │ ├── console-color-log.png │ ├── console-color-log1.png │ ├── console-log-all-level.png │ └── slog-all-level.png ├── issue100 │ └── issue100_test.go ├── issue111 │ └── main.go ├── issue137 │ └── main.go ├── pprof │ └── main.go └── refer │ └── main.go ├── benchmark2_test.go ├── benchmark_test.go ├── bufwrite ├── bufio_writer.go ├── bufwrite_test.go └── line_writer.go ├── common.go ├── common_test.go ├── example_test.go ├── formatter.go ├── formatter_json.go ├── formatter_test.go ├── formatter_text.go ├── go.mod ├── go.sum ├── handler.go ├── handler ├── README.md ├── buffer.go ├── buffer_test.go ├── builder.go ├── config.go ├── config_test.go ├── console.go ├── console_test.go ├── email.go ├── example_test.go ├── file.go ├── file_test.go ├── handler.go ├── handler_test.go ├── rotatefile.go ├── rotatefile_test.go ├── syslog.go ├── syslog_test.go ├── testdata │ └── .keep ├── write_close_flusher.go ├── write_close_syncer.go ├── write_closer.go ├── writer.go └── writer_test.go ├── handler_test.go ├── internal └── util.go ├── issues_test.go ├── logger.go ├── logger_test.go ├── logger_write.go ├── processor.go ├── processor_test.go ├── record.go ├── record_test.go ├── rotatefile ├── README.md ├── cleanup.go ├── cleanup_test.go ├── config.go ├── config_test.go ├── issues_test.go ├── rotatefile.go ├── rotatefile_test.go ├── util.go ├── util_test.go ├── writer.go └── writer_test.go ├── slog.go ├── slog_test.go ├── sugared.go ├── testdata ├── .keep └── runtime.Frame.txt ├── util.go └── util_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: inhere 7 | 8 | --- 9 | 10 | **System (please complete the following information):** 11 | 12 | - OS: `linux` [e.g. linux, macOS] 13 | - GO Version: `1.13` [e.g. `1.13`] 14 | - Pkg Version: `1.1.1` [e.g. `1.1.1`] 15 | 16 | **Describe the bug** 17 | 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | 22 | ```go 23 | // go code 24 | ``` 25 | 26 | **Expected behavior** 27 | 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/changelog.yml: -------------------------------------------------------------------------------- 1 | title: '## Change Log' 2 | # style allow: simple, markdown(mkdown), ghr(gh-release) 3 | style: gh-release 4 | # group names 5 | names: [Refactor, Fixed, Feature, Update, Other] 6 | repo_url: https://github.com/gookit/slog 7 | 8 | filters: 9 | # message length should >= 12 10 | - name: msg_len 11 | min_len: 12 12 | # message words should >= 3 13 | - name: words_len 14 | min_len: 3 15 | - name: keyword 16 | keyword: format code 17 | exclude: true 18 | - name: keywords 19 | keywords: format code, action test 20 | exclude: true 21 | 22 | # group match rules 23 | # not matched will use 'Other' group. 24 | rules: 25 | - name: Refactor 26 | start_withs: [refactor, break] 27 | contains: ['refactor:'] 28 | - name: Fixed 29 | start_withs: [fix] 30 | contains: ['fix:'] 31 | - name: Feature 32 | start_withs: [feat, new] 33 | contains: [feature] 34 | - name: Update 35 | start_withs: [update, 'up:'] 36 | contains: [] 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every weekday 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /.github/revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | # Sets the default severity to "warning" 3 | #severity = "error" 4 | severity = "warning" 5 | confidence = 0.8 6 | errorCode = 0 7 | warningCode = 0 8 | 9 | [rule.blank-imports] 10 | [rule.context-as-argument] 11 | [rule.context-keys-type] 12 | [rule.dot-imports] 13 | [rule.error-return] 14 | [rule.error-strings] 15 | [rule.error-naming] 16 | [rule.exported] 17 | severity = "warning" 18 | [rule.if-return] 19 | [rule.increment-decrement] 20 | [rule.var-naming] 21 | [rule.var-declaration] 22 | [rule.package-comments] 23 | [rule.range] 24 | [rule.receiver-naming] 25 | [rule.time-naming] 26 | [rule.unexported-return] 27 | [rule.indent-error-flow] 28 | [rule.errorf] 29 | [rule.argument-limit] 30 | arguments = [4] 31 | [rule.function-result-limit] 32 | arguments = [3] 33 | [rule.empty-block] 34 | [rule.confusing-naming] 35 | [rule.superfluous-else] 36 | [rule.unused-parameter] 37 | [rule.unreachable-code] 38 | [rule.unnecessary-stmt] 39 | [rule.struct-tag] 40 | [rule.atomic] 41 | [rule.empty-lines] 42 | [rule.duplicated-imports] 43 | [rule.import-shadowing] 44 | [rule.confusing-results] 45 | [rule.modifies-parameter] 46 | [rule.redefines-builtin-id] -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Unit-Tests 2 | on: 3 | pull_request: 4 | paths: 5 | - 'go.mod' 6 | - '**.go' 7 | - '**.yml' 8 | push: 9 | paths: 10 | - 'go.mod' 11 | - '**.go' 12 | - '**.yml' 13 | 14 | jobs: 15 | 16 | test: 17 | name: Test on go ${{ matrix.go_version }} and ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | go_version: [1.24, 1.23, 1.22, 1.21, 1.19, '1.20'] 22 | os: [ubuntu-latest, windows-latest] # , macOS-latest 23 | 24 | steps: 25 | - name: Check out code 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup Go Faster 29 | uses: WillAbides/setup-go-faster@v1.14.0 30 | timeout-minutes: 3 31 | with: 32 | go-version: ${{ matrix.go_version }} 33 | 34 | - name: Tidy go mod 35 | run: go mod tidy 36 | 37 | # https://github.com/actions/setup-go 38 | # - name: Use Go ${{ matrix.go_version }} 39 | # timeout-minutes: 3 40 | # uses: actions/setup-go@v3 41 | # with: 42 | # go-version: ${{ matrix.go_version }} 43 | 44 | # - name: Revive check 45 | # uses: docker://morphy/revive-action:v2 46 | # if: ${{ matrix.os == 'ubuntu-latest' && matrix.go_version == '1.22' }} 47 | # with: 48 | # config: .github/revive.toml 49 | # # Exclude patterns, separated by semicolons (optional) 50 | # exclude: "./internal/..." 51 | 52 | - name: Run staticcheck 53 | uses: reviewdog/action-staticcheck@v1 54 | if: ${{ github.event_name == 'pull_request'}} 55 | with: 56 | github_token: ${{ secrets.github_token }} 57 | # Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review]. 58 | reporter: github-pr-check 59 | # Report all results. [added,diff_context,file,nofilter]. 60 | filter_mode: added 61 | # Exit with 1 when it find at least one finding. 62 | fail_on_error: true 63 | 64 | - name: Run unit tests 65 | # run: go test -v -cover ./... 66 | run: go test -coverprofile="profile.cov" ./... 67 | 68 | - name: Send coverage 69 | uses: shogo82148/actions-goveralls@v1 70 | if: ${{ matrix.os == 'ubuntu-latest' }} 71 | with: 72 | path-to-profile: profile.cov 73 | flag-name: Go-${{ matrix.go_version }} 74 | parallel: true 75 | 76 | # notifies that all test jobs are finished. 77 | # https://github.com/shogo82148/actions-goveralls 78 | finish: 79 | needs: test 80 | runs-on: ubuntu-latest 81 | steps: 82 | - uses: shogo82148/actions-goveralls@v1 83 | with: 84 | parallel-finished: true 85 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Tag-release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | name: Release new version 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup ENV 21 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable 22 | run: | 23 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV 24 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV 25 | 26 | - name: Generate changelog 27 | run: | 28 | curl https://github.com/gookit/gitw/releases/latest/download/chlog-linux-amd64 -L -o /usr/local/bin/chlog 29 | chmod a+x /usr/local/bin/chlog 30 | chlog -c .github/changelog.yml -o changelog.md prev last 31 | 32 | # https://github.com/softprops/action-gh-release 33 | - name: Create release and upload assets 34 | uses: softprops/action-gh-release@v2 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | name: ${{ env.RELEASE_TAG }} 39 | tag_name: ${{ env.RELEASE_TAG }} 40 | body_path: changelog.md 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | # files: macos-chlog.exe 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.swp 3 | .idea 4 | *.patch 5 | *.tmp 6 | 7 | # Go template 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, build with `go test -c` 16 | *.test 17 | *.log.* 18 | *~ 19 | 20 | # Output of the go coverage tool, specifically when used with LiteIDE 21 | *.out 22 | .DS_Store 23 | *.prof 24 | 25 | # shell script 26 | /*.bash 27 | /*.sh 28 | /*.zsh 29 | /*.pid 30 | go.work 31 | changelog.md 32 | testdata 33 | _example/go.sum -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 inhere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /_example/bench_loglibs_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/gookit/slog" 8 | "github.com/gookit/slog/handler" 9 | phuslu "github.com/phuslu/log" 10 | "github.com/rs/zerolog" 11 | "github.com/sirupsen/logrus" 12 | "go.uber.org/zap" 13 | "go.uber.org/zap/zapcore" 14 | ) 15 | 16 | // In _example/ dir, run: 17 | // 18 | // go test -v -cpu=4 -run=none -bench=. -benchtime=10s -benchmem bench_loglibs_test.go 19 | // 20 | // code refer: 21 | // 22 | // https://github.com/phuslu/log 23 | var msg = "The quick brown fox jumps over the lazy dog" 24 | 25 | func BenchmarkZapNegative(b *testing.B) { 26 | logger := zap.New(zapcore.NewCore( 27 | zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()), 28 | zapcore.AddSync(io.Discard), 29 | zapcore.InfoLevel, 30 | )) 31 | 32 | b.ReportAllocs() 33 | b.ResetTimer() 34 | for i := 0; i < b.N; i++ { 35 | logger.Info(msg, zap.String("rate", "15"), zap.Int("low", 16), zap.Float32("high", 123.2)) 36 | } 37 | } 38 | 39 | func BenchmarkZapSugarNegative(b *testing.B) { 40 | logger := zap.New(zapcore.NewCore( 41 | zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()), 42 | zapcore.AddSync(io.Discard), 43 | // zapcore.AddSync(os.Stdout), 44 | zapcore.InfoLevel, 45 | )).Sugar() 46 | 47 | // logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 48 | // return 49 | 50 | b.ReportAllocs() 51 | b.ResetTimer() 52 | for i := 0; i < b.N; i++ { 53 | logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 54 | } 55 | } 56 | 57 | func BenchmarkZeroLogNegative(b *testing.B) { 58 | logger := zerolog.New(io.Discard).With().Timestamp().Logger().Level(zerolog.InfoLevel) 59 | 60 | b.ReportAllocs() 61 | b.ResetTimer() 62 | for i := 0; i < b.N; i++ { 63 | logger.Info().Str("rate", "15").Int("low", 16).Float32("high", 123.2).Msg(msg) 64 | } 65 | } 66 | 67 | func BenchmarkPhusLogNegative(b *testing.B) { 68 | logger := phuslu.Logger{Level: phuslu.InfoLevel, Writer: phuslu.IOWriter{Writer: io.Discard}} 69 | 70 | b.ReportAllocs() 71 | b.ResetTimer() 72 | for i := 0; i < b.N; i++ { 73 | logger.Info().Str("rate", "15").Int("low", 16).Float32("high", 123.2).Msg(msg) 74 | } 75 | } 76 | 77 | // "github.com/sirupsen/logrus" 78 | func BenchmarkLogrusNegative(b *testing.B) { 79 | logger := logrus.New() 80 | logger.Out = io.Discard 81 | logger.Level = logrus.InfoLevel 82 | 83 | b.ReportAllocs() 84 | b.ResetTimer() 85 | for i := 0; i < b.N; i++ { 86 | logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 87 | } 88 | } 89 | 90 | func BenchmarkGookitSlogNegative(b *testing.B) { 91 | logger := slog.NewWithHandlers( 92 | handler.NewIOWriter(io.Discard, []slog.Level{slog.InfoLevel}), 93 | // handler.NewIOWriter(os.Stdout, []slog.Level{slog.InfoLevel}), 94 | ) 95 | logger.ReportCaller = false 96 | 97 | // logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 98 | // return 99 | 100 | b.ReportAllocs() 101 | b.ResetTimer() 102 | 103 | for i := 0; i < b.N; i++ { 104 | logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 105 | } 106 | } 107 | 108 | func BenchmarkZapPositive(b *testing.B) { 109 | logger := zap.New(zapcore.NewCore( 110 | zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), 111 | zapcore.AddSync(io.Discard), 112 | zapcore.InfoLevel, 113 | )) 114 | 115 | b.ReportAllocs() 116 | b.ResetTimer() 117 | for i := 0; i < b.N; i++ { 118 | logger.Info(msg, zap.String("rate", "15"), zap.Int("low", 16), zap.Float32("high", 123.2)) 119 | } 120 | } 121 | 122 | func BenchmarkZapSugarPositive(b *testing.B) { 123 | logger := zap.New(zapcore.NewCore( 124 | zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()), 125 | zapcore.AddSync(io.Discard), 126 | zapcore.InfoLevel, 127 | )).Sugar() 128 | 129 | b.ReportAllocs() 130 | b.ResetTimer() 131 | for i := 0; i < b.N; i++ { 132 | logger.Info(msg, zap.String("rate", "15"), zap.Int("low", 16), zap.Float32("high", 123.2)) 133 | } 134 | } 135 | 136 | func BenchmarkZeroLogPositive(b *testing.B) { 137 | logger := zerolog.New(io.Discard).With().Timestamp().Logger().Level(zerolog.InfoLevel) 138 | 139 | b.ReportAllocs() 140 | b.ResetTimer() 141 | for i := 0; i < b.N; i++ { 142 | logger.Info().Str("rate", "15").Int("low", 16).Float32("high", 123.2).Msg(msg) 143 | } 144 | } 145 | 146 | func BenchmarkPhusLogPositive(b *testing.B) { 147 | logger := phuslu.Logger{Level: phuslu.InfoLevel, Writer: phuslu.IOWriter{Writer: io.Discard}} 148 | 149 | b.ReportAllocs() 150 | b.ResetTimer() 151 | for i := 0; i < b.N; i++ { 152 | logger.Info().Str("rate", "15").Int("low", 16).Float32("high", 123.2).Msg(msg) 153 | } 154 | } 155 | 156 | func BenchmarkLogrusPositive(b *testing.B) { 157 | logger := logrus.New() 158 | logger.Out = io.Discard 159 | logger.Level = logrus.InfoLevel 160 | 161 | b.ReportAllocs() 162 | b.ResetTimer() 163 | for i := 0; i < b.N; i++ { 164 | logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 165 | } 166 | } 167 | 168 | func BenchmarkGookitSlogPositive(b *testing.B) { 169 | logger := slog.NewWithHandlers( 170 | handler.NewIOWriter(io.Discard, []slog.Level{slog.InfoLevel}), 171 | ) 172 | logger.ReportCaller = false 173 | 174 | b.ReportAllocs() 175 | b.ResetTimer() 176 | for i := 0; i < b.N; i++ { 177 | logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /_example/demos/demo1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/gookit/slog" 5 | ) 6 | 7 | const simplestTemplate = "[{{datetime}}] [{{level}}] {{message}} {{data}} {{extra}}" 8 | 9 | func init() { 10 | log.GetFormatter().(*log.TextFormatter).SetTemplate(simplestTemplate) 11 | log.SetLogLevel(log.ErrorLevel) 12 | log.Errorf("Test") 13 | } 14 | 15 | func main() { 16 | } 17 | -------------------------------------------------------------------------------- /_example/demos/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/gookit/slog" 4 | 5 | // profile run: 6 | // 7 | // go build -gcflags '-m -l' simple.go 8 | func main() { 9 | // stackIt() 10 | // _ = stackIt2() 11 | slogTest() 12 | } 13 | 14 | //go:noinline 15 | func stackIt() int { 16 | y := 2 17 | return y * 2 18 | } 19 | 20 | //go:noinline 21 | func stackIt2() *int { 22 | y := 2 23 | res := y * 2 24 | return &res 25 | } 26 | 27 | func slogTest() { 28 | var msg = "The quick brown fox jumps over the lazy dog" 29 | 30 | slog.Info("rate", "15", "low", 16, "high", 123.2, msg) 31 | // slog.WithFields(slog.M{ 32 | // "omg": true, 33 | // "number": 122, 34 | // }).Infof("slog %s", "message message") 35 | } 36 | -------------------------------------------------------------------------------- /_example/demos/slog_all_level.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gookit/goutil/errorx" 7 | "github.com/gookit/slog" 8 | "github.com/gookit/slog/handler" 9 | ) 10 | 11 | // run: go run ./_example/slog_all_level.go 12 | func main() { 13 | l := slog.NewWithConfig(func(l *slog.Logger) { 14 | l.DoNothingOnPanicFatal() 15 | }) 16 | 17 | l.AddHandler(handler.NewConsoleHandler(slog.AllLevels)) 18 | printAllLevel(l, "this is a", "log", "message") 19 | } 20 | 21 | func printAllLevel(l *slog.Logger, args ...any) { 22 | l.Debug(args...) 23 | l.Info(args...) 24 | l.Warn(args...) 25 | l.Error(args...) 26 | l.Print(args...) 27 | l.Fatal(args...) 28 | l.Panic(args...) 29 | 30 | l.Trace(args...) 31 | l.Notice(args...) 32 | l.ErrorT(errors.New("a error object")) 33 | l.ErrorT(errorx.New("error with stack info")) 34 | } 35 | -------------------------------------------------------------------------------- /_example/diff-with-zap-zerolog.md: -------------------------------------------------------------------------------- 1 | # diff with zap, zerolog 2 | 3 | 是的,zap 非常快速。 4 | 5 | 但是有一点问题: 6 | 7 | - 配置起来稍显复杂 8 | - 没有内置切割文件处理和文件清理 9 | - 自定义扩展性不是很好 10 | 11 | Yes, zap is very fast. 12 | 13 | But there is a little problem: 14 | 15 | - Slightly complicated to configure 16 | - No built-in cutting file handling, file cleanup 17 | - Custom extensibility is not very good -------------------------------------------------------------------------------- /_example/go.mod: -------------------------------------------------------------------------------- 1 | module slog_example 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/golang/glog v1.2.4 7 | github.com/gookit/goutil v0.6.18 8 | github.com/gookit/slog v0.5.0 9 | github.com/phuslu/log v1.0.113 10 | github.com/rs/zerolog v1.33.0 11 | github.com/sirupsen/logrus v1.9.3 12 | github.com/syyongx/llog v0.0.0-20200222114215-e8f9f86ac0a3 13 | go.uber.org/zap v1.27.0 14 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 15 | ) 16 | 17 | require ( 18 | github.com/gookit/color v1.5.4 // indirect 19 | github.com/gookit/gsr v0.1.0 // indirect 20 | github.com/mattn/go-colorable v0.1.13 // indirect 21 | github.com/mattn/go-isatty v0.0.19 // indirect 22 | github.com/valyala/bytebufferpool v1.0.0 // indirect 23 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 24 | go.uber.org/multierr v1.11.0 // indirect 25 | golang.org/x/sync v0.10.0 // indirect 26 | golang.org/x/sys v0.28.0 // indirect 27 | golang.org/x/text v0.21.0 // indirect 28 | ) 29 | 30 | replace github.com/gookit/slog => ../ 31 | -------------------------------------------------------------------------------- /_example/handler/grouped.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import "github.com/gookit/slog" 4 | 5 | /******************************************************************************** 6 | * Grouped Handler 7 | ********************************************************************************/ 8 | 9 | // GroupedHandler definition 10 | type GroupedHandler struct { 11 | handlers []slog.Handler 12 | // Levels for log message 13 | Levels []slog.Level 14 | // IgnoreErr on handling messages 15 | IgnoreErr bool 16 | } 17 | 18 | // NewGroupedHandler create new GroupedHandler 19 | func NewGroupedHandler(handlers []slog.Handler) *GroupedHandler { 20 | return &GroupedHandler{ 21 | handlers: handlers, 22 | } 23 | } 24 | 25 | // IsHandling Check if the current level can be handling 26 | func (h *GroupedHandler) IsHandling(level slog.Level) bool { 27 | for _, l := range h.Levels { 28 | if l == level { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | // Handle log record 36 | func (h *GroupedHandler) Handle(record *slog.Record) (err error) { 37 | for _, handler := range h.handlers { 38 | err = handler.Handle(record) 39 | if !h.IgnoreErr && err != nil { 40 | return err 41 | } 42 | } 43 | return 44 | } 45 | 46 | // Close log handlers 47 | func (h *GroupedHandler) Close() error { 48 | for _, handler := range h.handlers { 49 | err := handler.Close() 50 | if !h.IgnoreErr && err != nil { 51 | return err 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | // Flush log records 58 | func (h *GroupedHandler) Flush() error { 59 | for _, handler := range h.handlers { 60 | err := handler.Flush() 61 | if !h.IgnoreErr && err != nil { 62 | return err 63 | } 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /_example/handler/multi_file.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gookit/slog" 5 | ) 6 | 7 | // MultiFileHandler definition TODO 8 | type MultiFileHandler struct { 9 | LockWrapper 10 | // writers map[string]io.Writer 11 | // FileDir for save log files 12 | FileDir string 13 | // FileLevels can use multi file for record level logs. eg: 14 | // 15 | // "error.log": []slog.Level{slog.Warn, slog.Error}, 16 | // "info.log": []slog.Level{slog.Trace, slog.Info, slog.Notice} 17 | FileLevels map[string]slog.Levels 18 | // NoBuffer on write log records 19 | NoBuffer bool 20 | // BuffSize for enable buffer 21 | BuffSize int 22 | // file contents max size 23 | MaxSize uint64 24 | } 25 | 26 | // NewMultiFileHandler instance 27 | func NewMultiFileHandler() *MultiFileHandler { 28 | return &MultiFileHandler{} 29 | } 30 | 31 | // IsHandling Check if the current level can be handling 32 | func (h *MultiFileHandler) IsHandling(level slog.Level) bool { 33 | for _, ls := range h.FileLevels { 34 | if ls.Contains(level) { 35 | return true 36 | } 37 | } 38 | return false 39 | } 40 | 41 | // Close handle 42 | func (h *MultiFileHandler) Close() error { 43 | panic("implement me") 44 | } 45 | 46 | // Flush handle 47 | func (h *MultiFileHandler) Flush() error { 48 | panic("implement me") 49 | } 50 | 51 | // Handle log record 52 | func (h *MultiFileHandler) Handle(_ *slog.Record) error { 53 | panic("implement me") 54 | } 55 | -------------------------------------------------------------------------------- /_example/images/console-color-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/slog/90c4be86a3060e094e279af7009139357eaf41a5/_example/images/console-color-log.png -------------------------------------------------------------------------------- /_example/images/console-color-log1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/slog/90c4be86a3060e094e279af7009139357eaf41a5/_example/images/console-color-log1.png -------------------------------------------------------------------------------- /_example/images/console-log-all-level.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/slog/90c4be86a3060e094e279af7009139357eaf41a5/_example/images/console-log-all-level.png -------------------------------------------------------------------------------- /_example/images/slog-all-level.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/slog/90c4be86a3060e094e279af7009139357eaf41a5/_example/images/slog-all-level.png -------------------------------------------------------------------------------- /_example/issue100/issue100_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gookit/slog" 9 | "github.com/gookit/slog/handler" 10 | "go.uber.org/zap" 11 | "go.uber.org/zap/zapcore" 12 | "gopkg.in/natefinch/lumberjack.v2" 13 | ) 14 | 15 | type Obj struct { 16 | a int 17 | b int64 18 | c string 19 | d bool 20 | } 21 | 22 | var ( 23 | str1 = "str1" 24 | str2 = "str222222222222" 25 | int1 = 1 26 | int2 = 2 27 | obj = Obj{1, 2, "3", true} 28 | ) 29 | 30 | func TestZapSugar(t *testing.T) { 31 | w := zapcore.AddSync(&lumberjack.Logger{ 32 | Filename: "./zap-sugar.log", 33 | MaxSize: 500, // megabytes 34 | MaxBackups: 3, 35 | MaxAge: 28, // days 36 | }) 37 | core := zapcore.NewCore( 38 | zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), 39 | w, 40 | zap.InfoLevel, 41 | ) 42 | logger := zap.New(core) 43 | 44 | sugar := logger.Sugar() 45 | sugar.Info("message is msg") 46 | 47 | count := 100000 48 | start := time.Now().UnixNano() 49 | for n := count; n > 0; n-- { 50 | sugar.Info("message is msg") 51 | } 52 | end := time.Now().UnixNano() 53 | fmt.Printf("\n zap sugar no format\n total cost %d ns\n avg cost %d ns \n count %d \n", end-start, (end-start)/int64(count), count) 54 | 55 | start = time.Now().UnixNano() 56 | for n := count; n > 0; n-- { 57 | sugar.Infof("message is %d %d %s %s %#v", int1, int2, str1, str2, obj) 58 | } 59 | end = time.Now().UnixNano() 60 | fmt.Printf("\n zap sugar format\n total cost %d ns\n avg cost %d ns \n count %d \n", end-start, (end-start)/int64(count), count) 61 | sugar.Sync() 62 | } 63 | 64 | func TestZapLog(t *testing.T) { 65 | w := zapcore.AddSync(&lumberjack.Logger{ 66 | Filename: "./zap.log", 67 | MaxSize: 500, // megabytes 68 | MaxBackups: 3, 69 | MaxAge: 28, // days 70 | }) 71 | 72 | core := zapcore.NewCore( 73 | zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), 74 | w, 75 | zap.InfoLevel, 76 | ) 77 | logger := zap.New(core) 78 | 79 | count := 100000 80 | start := time.Now().UnixNano() 81 | for n := count; n > 0; n-- { 82 | logger.Info("message is msg") 83 | } 84 | end := time.Now().UnixNano() 85 | fmt.Printf("\n zap no format\n total cost %d ns\n avg cost %d ns \n count %d \n", end-start, (end-start)/int64(count), count) 86 | 87 | start = time.Now().UnixNano() 88 | for n := count; n > 0; n-- { 89 | logger.Info("failed to fetch URL", 90 | // Structured context as strongly typed Field values. 91 | zap.Int("int1", int1), 92 | zap.Int("int2", int2), 93 | zap.String("str", str1), 94 | zap.String("str2", str2), 95 | zap.Any("backoff", obj), 96 | ) 97 | } 98 | end = time.Now().UnixNano() 99 | fmt.Printf("\n zap format\n total cost %d ns\n avg cost %d ns \n count %d \n", end-start, (end-start)/int64(count), count) 100 | logger.Sync() 101 | } 102 | 103 | func TestSlog(t *testing.T) { 104 | h1, err := handler.NewEmptyConfig( 105 | handler.WithLogfile("./slog-info.log"), // 路径 106 | handler.WithRotateTime(handler.EveryHour), // 日志分割间隔 107 | handler.WithLogLevels(slog.AllLevels), // 日志level 108 | handler.WithBuffSize(4*1024*1024), // buffer大小 109 | handler.WithCompress(true), // 是否压缩旧日志 zip 110 | handler.WithBackupNum(24*3), // 保留旧日志数量 111 | handler.WithBuffMode(handler.BuffModeBite), 112 | // handler.WithRenameFunc(), //RenameFunc build filename for rotate file 113 | ).CreateHandler() 114 | if err != nil { 115 | fmt.Printf("Create slog handler err: %#v", err) 116 | return 117 | } 118 | 119 | f := slog.AsTextFormatter(h1.Formatter()) 120 | myTplt := "[{{datetime}}] [{{level}}] [{{caller}}] {{message}}\n" 121 | f.SetTemplate(myTplt) 122 | logs := slog.NewWithHandlers(h1) 123 | 124 | count := 100000 125 | start := time.Now().UnixNano() 126 | for i := 0; i < count; i++ { 127 | logs.Info("message is msg") 128 | } 129 | end := time.Now().UnixNano() 130 | fmt.Printf("\n slog no format \n total cost %d ns\n avg cost %d ns \n count %d \n", end-start, (end-start)/int64(count), count) 131 | 132 | start = time.Now().UnixNano() 133 | for n := count; n > 0; n-- { 134 | logs.Infof("message is %d %d %s %s %#v", int1, int2, str1, str2, obj) 135 | } 136 | end = time.Now().UnixNano() 137 | fmt.Printf("\n slog format \n total cost %d ns\n avg cost %d ns \n count %d \n", end-start, (end-start)/int64(count), count) 138 | logs.MustClose() 139 | } 140 | -------------------------------------------------------------------------------- /_example/issue111/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/gookit/goutil/syncs" 9 | "github.com/gookit/goutil/timex" 10 | "github.com/gookit/slog" 11 | "github.com/gookit/slog/handler" 12 | "github.com/gookit/slog/rotatefile" 13 | ) 14 | 15 | const pth = "./logs/main.log" 16 | 17 | func main() { 18 | log := slog.New() 19 | 20 | h, err := handler.NewTimeRotateFileHandler( 21 | pth, 22 | rotatefile.RotateTime(30), 23 | handler.WithBuffSize(0), 24 | handler.WithBackupNum(5), 25 | handler.WithCompress(true), 26 | func(c *handler.Config) { 27 | c.DebugMode = true 28 | }, 29 | ) 30 | 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | log.AddHandler(h) 36 | 37 | fmt.Println("Start...(can be stop by CTRL+C)", timex.NowDate()) 38 | go func() { 39 | for { 40 | select { 41 | case <-time.After(time.Second): 42 | log.Info("Log " + time.Now().String()) 43 | } 44 | } 45 | }() 46 | 47 | syncs.WaitCloseSignals(func(sig os.Signal) { 48 | fmt.Println("\nGot signal:", sig) 49 | fmt.Println("Close logger ...") 50 | log.MustClose() 51 | }) 52 | 53 | fmt.Println("Exited at", timex.NowDate()) 54 | } 55 | -------------------------------------------------------------------------------- /_example/issue137/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "time" 7 | 8 | "github.com/gookit/slog" 9 | "github.com/gookit/slog/handler" 10 | "github.com/gookit/slog/rotatefile" 11 | ) 12 | 13 | type GLogConfig137 struct { 14 | Level string `yaml:"Level"` 15 | Pattern string `yaml:"Pattern"` 16 | TimeField string `yaml:"TimeField"` 17 | TimeFormat string `yaml:"TimeFormat"` 18 | Template string `yaml:"Template"` 19 | RotateTimeFormat string `yaml:"RotateTimeFormat"` 20 | } 21 | 22 | type LogRotateConfig137 struct { 23 | Filepath string `yaml:"filepath"` 24 | RotateMode rotatefile.RotateMode `yaml:"rotate_mode"` 25 | RotateTime rotatefile.RotateTime `yaml:"rotate_time"` 26 | MaxSize uint64 `yaml:"max_size"` 27 | BackupNum uint `yaml:"backup_num"` 28 | BackupTime uint `yaml:"backup_time"` 29 | Compress bool `yaml:"compress"` 30 | TimeFormat string `yaml:"time_format"` 31 | BuffSize int `yaml:"buff_size"` 32 | BuffMode string `yaml:"buff_mode"` 33 | } 34 | 35 | type LogConfig137 struct { 36 | GLogConfig GLogConfig137 `yaml:"GLogConfig"` 37 | LogRotate LogRotateConfig137 `yaml:"LogRotate"` 38 | ErrorLogRotate LogRotateConfig137 `yaml:"ErrorLogRotate"` 39 | } 40 | 41 | func main() { 42 | slog.DebugMode = true 43 | 44 | logConfig := LogConfig137{ 45 | GLogConfig: GLogConfig137{ 46 | Level: "debug", 47 | Pattern: "development", 48 | TimeField: "time", 49 | TimeFormat: "2006-01-02 15:04:05.000", 50 | Template: "{{datetime}}\t{{level}}\t{{channel}}\t[{{caller}}]\t{{message}}\t{{data}}\t{{extra}}\n", 51 | RotateTimeFormat: "20060102", 52 | }, 53 | LogRotate: LogRotateConfig137{ 54 | Filepath: "testdata/info137c2.log", 55 | RotateMode: 0, 56 | RotateTime: 86400, 57 | MaxSize: 512, 58 | BackupNum: 3, 59 | BackupTime: 72, 60 | Compress: true, 61 | TimeFormat: "20060102", 62 | BuffSize: 512, 63 | BuffMode: "line", 64 | }, 65 | ErrorLogRotate: LogRotateConfig137{ 66 | Filepath: "testdata/err137c2.log", 67 | RotateMode: 0, 68 | RotateTime: 86400, 69 | MaxSize: 512, 70 | BackupNum: 3, 71 | BackupTime: 72, 72 | Compress: true, 73 | TimeFormat: "20060102", 74 | BuffSize: 512, 75 | BuffMode: "line", 76 | }, 77 | } 78 | tpl := logConfig.GLogConfig.Template 79 | 80 | // slog.DefaultChannelName = "gookit" 81 | slog.DefaultTimeFormat = logConfig.GLogConfig.TimeFormat 82 | 83 | slog.Configure(func(l *slog.SugaredLogger) { 84 | l.Level = slog.TraceLevel 85 | l.DoNothingOnPanicFatal() 86 | l.ChannelName = "gookit" 87 | }) 88 | slog.GetFormatter().(*slog.TextFormatter).SetTemplate(tpl) 89 | slog.GetFormatter().(*slog.TextFormatter).TimeFormat = slog.DefaultTimeFormat 90 | 91 | rotatefile.DefaultFilenameFn = func(filepath string, rotateNum uint) string { 92 | suffix := time.Now().Format(logConfig.GLogConfig.RotateTimeFormat) 93 | 94 | // eg: /tmp/error.log => /tmp/error_20250302_01.log 95 | // 将文件名扩展名取出来, 然后在扩展名中间加入下划线+日期+下划线+序号+扩展名的形式 96 | ext := path.Ext(filepath) 97 | filename := filepath[:len(filepath)-len(ext)] 98 | 99 | return filename + fmt.Sprintf("_%s_%02d", suffix, rotateNum) + ext 100 | } 101 | 102 | h1 := handler.MustRotateFile(logConfig.ErrorLogRotate.Filepath, 103 | logConfig.ErrorLogRotate.RotateTime, 104 | // handler.WithFilePerm(os.ModeAppend|os.ModePerm), 105 | handler.WithLevelMode(slog.LevelModeList), 106 | handler.WithLogLevels(slog.DangerLevels), 107 | handler.WithMaxSize(logConfig.ErrorLogRotate.MaxSize), 108 | handler.WithBackupNum(logConfig.ErrorLogRotate.BackupNum), 109 | handler.WithBackupTime(logConfig.ErrorLogRotate.BackupTime), 110 | handler.WithCompress(logConfig.ErrorLogRotate.Compress), 111 | handler.WithBuffSize(logConfig.ErrorLogRotate.BuffSize), 112 | handler.WithBuffMode(logConfig.ErrorLogRotate.BuffMode), 113 | handler.WithRotateMode(logConfig.ErrorLogRotate.RotateMode), 114 | ) 115 | h1.Formatter().(*slog.TextFormatter).SetTemplate(tpl) 116 | 117 | h2 := handler.MustRotateFile(logConfig.LogRotate.Filepath, 118 | logConfig.LogRotate.RotateTime, 119 | // handler.WithFilePerm(os.ModeAppend|os.ModePerm), 120 | handler.WithLevelMode(slog.LevelModeList), 121 | handler.WithLogLevels(slog.AllLevels), 122 | handler.WithMaxSize(logConfig.LogRotate.MaxSize), 123 | handler.WithBackupNum(logConfig.LogRotate.BackupNum), 124 | handler.WithBackupTime(logConfig.LogRotate.BackupTime), 125 | handler.WithCompress(logConfig.LogRotate.Compress), 126 | handler.WithBuffSize(logConfig.LogRotate.BuffSize), 127 | handler.WithBuffMode(logConfig.LogRotate.BuffMode), 128 | handler.WithRotateMode(logConfig.LogRotate.RotateMode), 129 | ) 130 | h2.Formatter().(*slog.TextFormatter).SetTemplate(tpl) 131 | 132 | slog.PushHandlers(h1, h2) 133 | 134 | // add logs 135 | for i := 0; i < 20; i++ { 136 | slog.Infof("hi, this is a example information ... message text. log index=%d", i) 137 | slog.WithValue("test137", "some value").Warn("测试滚动多个文件,同时设置了清理日志文件") 138 | } 139 | 140 | slog.MustClose() 141 | time.Sleep(time.Second * 2) 142 | } 143 | -------------------------------------------------------------------------------- /_example/pprof/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "runtime/pprof" 9 | 10 | "github.com/gookit/slog" 11 | "github.com/gookit/slog/handler" 12 | ) 13 | 14 | // run serve: 15 | // 16 | // go run ./_examples/pprof 17 | // 18 | // see prof on cli: 19 | // 20 | // go tool pprof pprof/cpu_prof_data.out 21 | // 22 | // see prof on web: 23 | // 24 | // go tool pprof -http=:8080 pprof/cpu_prof_data.out 25 | func main() { 26 | logger := slog.NewWithHandlers( 27 | handler.NewIOWriter(io.Discard, slog.NormalLevels), 28 | ) 29 | 30 | times := 10000 31 | fmt.Println("start profile, run times:", times) 32 | 33 | cpuProfile := "cpu_prof_data.out" 34 | f, err := os.Create(cpuProfile) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | err = pprof.StartCPUProfile(f) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | defer pprof.StopCPUProfile() 45 | 46 | var msg = "The quick brown fox jumps over the lazy dog" 47 | for i := 0; i < times; i++ { 48 | logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 49 | } 50 | 51 | fmt.Println("see prof on web:\n go tool pprof -http=:8080", cpuProfile) 52 | } 53 | -------------------------------------------------------------------------------- /_example/refer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "time" 7 | 8 | "github.com/golang/glog" 9 | "github.com/gookit/slog" 10 | "github.com/sirupsen/logrus" 11 | 12 | "github.com/syyongx/llog" 13 | 14 | "go.uber.org/zap" 15 | 16 | "github.com/rs/zerolog" 17 | zlog "github.com/rs/zerolog/log" 18 | ) 19 | 20 | func main() { 21 | // for glog 22 | flag.Parse() 23 | 24 | // -- log 25 | log.Println("raw log message") 26 | 27 | // -- glog 28 | glog.Infof("glog %s", "message message") 29 | 30 | // -- llog 31 | llog.NewLogger("llog test").Info("llog message message") 32 | 33 | // -- slog 34 | slog.Debug("slog message message") 35 | slog.WithFields(slog.M{ 36 | "omg": true, 37 | "number": 122, 38 | }).Infof("slog %s", "message message") 39 | 40 | // -- logrus 41 | logrus.Debug("logrus message message") 42 | logrus.WithFields(logrus.Fields{ 43 | "omg": true, 44 | "number": 122, 45 | }).Warn("The group's number increased tremendously!") 46 | 47 | // -- zerolog 48 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix 49 | zlog.Debug(). 50 | Str("Scale", "833 cents"). 51 | Float64("Interval", 833.09). 52 | Msg("zerolog message") 53 | zlog.Print("zerolog hello") 54 | 55 | // slog.Infof("log %s", "message") 56 | url := "/path/to/some" 57 | 58 | // -- zap 59 | logger, _ := zap.NewProduction() 60 | defer logger.Sync() // flushes buffer, if any 61 | sugar := logger.Sugar() 62 | sugar.Infow("failed to fetch URL", 63 | // Structured context as loosely typed key-value pairs. 64 | "url", url, 65 | "attempt", 3, 66 | "backoff", time.Second, 67 | ) 68 | sugar.Infof("zap log. Failed to fetch URL: %s", url) 69 | } 70 | -------------------------------------------------------------------------------- /benchmark2_test.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/dump" 9 | ) 10 | 11 | func TestLogger_newRecord_AllocTimes(_ *testing.T) { 12 | l := Std() 13 | l.Output = io.Discard 14 | defer l.Reset() 15 | 16 | // output: 0 times 17 | fmt.Println("Alloc Times:", int(testing.AllocsPerRun(100, func() { 18 | // logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 19 | r := l.newRecord() 20 | // do something... 21 | l.releaseRecord(r) 22 | }))) 23 | } 24 | 25 | func Test_formatArgsWithSpaces_oneElem_AllocTimes(_ *testing.T) { 26 | // output: 1 times -> 0 times 27 | fmt.Println("Alloc Times:", int(testing.AllocsPerRun(10, func() { 28 | // logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 29 | formatArgsWithSpaces([]any{ 30 | "msg", // 2343, -23, 123.2, 31 | }) 32 | }))) 33 | } 34 | 35 | func Test_AllocTimes_formatArgsWithSpaces_manyElem(_ *testing.T) { 36 | l := Std() 37 | l.Output = io.Discard 38 | defer l.Reset() 39 | 40 | // TIP: 41 | // `float` will alloc 2 times memory 42 | // `int <0`, `int > 100` will alloc 1 times memory 43 | fmt.Println("Alloc Times:", int(testing.AllocsPerRun(100, func() { 44 | // logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 45 | formatArgsWithSpaces([]any{ 46 | "rate", -23, true, 106, "high", 123.2, 47 | }) 48 | }))) 49 | } 50 | 51 | func Test_AllocTimes_stringsPool(_ *testing.T) { 52 | l := Std() 53 | l.Output = io.Discard 54 | l.LowerLevelName = true 55 | defer l.Reset() 56 | 57 | var ln, cp int 58 | // output: 0 times 59 | fmt.Println("Alloc Times:", int(testing.AllocsPerRun(100, func() { 60 | // logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 61 | 62 | // oldnew := stringsPool.Get().([]string) 63 | // defer stringsPool.Put(oldnew) 64 | 65 | oldnew := make([]string, 0, len(map[string]string{"a": "b"})*2+1) 66 | 67 | oldnew = append(oldnew, "a") 68 | oldnew = append(oldnew, "b") 69 | oldnew = append(oldnew, "c") 70 | // oldnew = append(oldnew, "d") 71 | 72 | ln = len(oldnew) 73 | cp = cap(oldnew) 74 | }))) 75 | 76 | dump.P(ln, cp) 77 | } 78 | 79 | func TestLogger_Info_oneElem_AllocTimes(_ *testing.T) { 80 | l := Std() 81 | // l.Output = io.Discard 82 | l.ReportCaller = false 83 | l.LowerLevelName = true 84 | // 启用 color 会导致多次(10次左右)内存分配 85 | l.Formatter.(*TextFormatter).EnableColor = false 86 | 87 | defer l.Reset() 88 | 89 | // output: 2 times 90 | fmt.Println("Alloc Times:", int(testing.AllocsPerRun(5, func() { 91 | // l.Info("rate", "15", "low", 16, "high", 123.2, "msg") 92 | l.Info("msg") 93 | }))) 94 | } 95 | 96 | func TestLogger_Info_moreElem_AllocTimes(_ *testing.T) { 97 | l := NewStdLogger() 98 | // l.Output = io.Discard 99 | l.ReportCaller = false 100 | l.LowerLevelName = true 101 | // 启用 color 会导致多次(10次左右)内存分配 102 | l.Formatter.(*TextFormatter).EnableColor = false 103 | 104 | defer l.Reset() 105 | 106 | // output: 5 times 107 | fmt.Println("Alloc Times:", int(testing.AllocsPerRun(5, func() { 108 | l.Info("rate", "15", "low", 16, "high", 123.2, "msg") 109 | }))) 110 | 111 | // output: 5 times 112 | fmt.Println("Alloc Times:", int(testing.AllocsPerRun(5, func() { 113 | l.Info("rate", "15", "low", 16, "high") 114 | // l.Info("msg") 115 | }))) 116 | } 117 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package slog_test 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/gookit/goutil/dump" 8 | "github.com/gookit/slog" 9 | "github.com/gookit/slog/handler" 10 | ) 11 | 12 | // go test -v -cpu=4 -run=none -bench=. -benchtime=10s -benchmem bench_test.go 13 | // 14 | // code refer: 15 | // 16 | // https://github.com/phuslu/log 17 | var msg = "The quick brown fox jumps over the lazy dog" 18 | 19 | func BenchmarkGookitSlogNegative(b *testing.B) { 20 | logger := slog.NewWithHandlers( 21 | handler.NewIOWriter(io.Discard, []slog.Level{slog.ErrorLevel}), 22 | ) 23 | 24 | b.ReportAllocs() 25 | b.ResetTimer() 26 | 27 | for i := 0; i < b.N; i++ { 28 | logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 29 | } 30 | } 31 | 32 | func TestLogger_Info_Negative(t *testing.T) { 33 | logger := slog.NewWithHandlers( 34 | handler.NewIOWriter(io.Discard, []slog.Level{slog.ErrorLevel}), 35 | ) 36 | 37 | logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 38 | } 39 | 40 | func BenchmarkGookitSlogPositive(b *testing.B) { 41 | logger := slog.NewWithHandlers( 42 | handler.NewIOWriter(io.Discard, slog.NormalLevels), 43 | ) 44 | 45 | b.ReportAllocs() 46 | b.ResetTimer() 47 | 48 | for i := 0; i < b.N; i++ { 49 | logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 50 | } 51 | } 52 | 53 | func BenchmarkTextFormatter_Format(b *testing.B) { 54 | r := newLogRecord("TEST_LOG_MESSAGE") 55 | f := slog.NewTextFormatter() 56 | // 1284 ns/op 456 B/op 11 allocs/op 57 | // On use DefaultTemplate 58 | 59 | // 304.4 ns/op 200 B/op 2 allocs/op 60 | // f.SetTemplate("{{datetime}} {{message}}") 61 | 62 | // 271.3 ns/op 200 B/op 2 allocs/op 63 | // f.SetTemplate("{{datetime}}") 64 | // f.SetTemplate("{{message}}") 65 | dump.P(f.Template()) 66 | 67 | b.ReportAllocs() 68 | b.ResetTimer() 69 | 70 | for i := 0; i < b.N; i++ { 71 | _, err := f.Format(r) 72 | if err != nil { 73 | panic(err) 74 | } 75 | } 76 | } 77 | 78 | func TestLogger_Info_Positive(t *testing.T) { 79 | logger := slog.NewWithHandlers( 80 | handler.NewIOWriter(io.Discard, slog.NormalLevels), 81 | ) 82 | 83 | logger.Info("rate", "15", "low", 16, "high", 123.2, msg) 84 | } 85 | -------------------------------------------------------------------------------- /bufwrite/bufio_writer.go: -------------------------------------------------------------------------------- 1 | // Package bufwrite provides buffered io.Writer with sync and close methods. 2 | package bufwrite 3 | 4 | import ( 5 | "bufio" 6 | "io" 7 | ) 8 | 9 | // BufIOWriter wrap the bufio.Writer, implements the Sync() Close() methods 10 | type BufIOWriter struct { 11 | bufio.Writer 12 | // backup the bufio.Writer.wr 13 | writer io.Writer 14 | } 15 | 16 | // NewBufIOWriterSize instance with size 17 | func NewBufIOWriterSize(w io.Writer, size int) *BufIOWriter { 18 | return &BufIOWriter{ 19 | writer: w, 20 | Writer: *bufio.NewWriterSize(w, size), 21 | } 22 | } 23 | 24 | // NewBufIOWriter instance 25 | func NewBufIOWriter(w io.Writer) *BufIOWriter { 26 | return NewBufIOWriterSize(w, defaultBufSize) 27 | } 28 | 29 | // Close implements the io.Closer 30 | func (w *BufIOWriter) Close() error { 31 | if err := w.Flush(); err != nil { 32 | return err 33 | } 34 | 35 | // is closer 36 | if c, ok := w.writer.(io.Closer); ok { 37 | return c.Close() 38 | } 39 | return nil 40 | } 41 | 42 | // Sync implements the Syncer 43 | func (w *BufIOWriter) Sync() error { 44 | return w.Flush() 45 | } 46 | -------------------------------------------------------------------------------- /bufwrite/bufwrite_test.go: -------------------------------------------------------------------------------- 1 | package bufwrite_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/gookit/goutil/errorx" 8 | "github.com/gookit/goutil/testutil/assert" 9 | "github.com/gookit/slog/bufwrite" 10 | ) 11 | 12 | func TestNewBufIOWriter_WriteString(t *testing.T) { 13 | w := new(bytes.Buffer) 14 | bw := bufwrite.NewBufIOWriterSize(w, 12) 15 | 16 | _, err := bw.WriteString("hello, ") 17 | assert.NoErr(t, err) 18 | assert.Eq(t, 0, w.Len()) 19 | 20 | _, err = bw.WriteString("worlds. oh") 21 | assert.NoErr(t, err) 22 | assert.Eq(t, "hello, world", w.String()) // different the LineWriter 23 | 24 | assert.NoErr(t, bw.Close()) 25 | assert.Eq(t, "hello, worlds. oh", w.String()) 26 | } 27 | 28 | type closeWriter struct { 29 | errOnWrite bool 30 | errOnClose bool 31 | writeNum int 32 | } 33 | 34 | func (w *closeWriter) Close() error { 35 | if w.errOnClose { 36 | return errorx.Raw("close error") 37 | } 38 | return nil 39 | } 40 | 41 | func (w *closeWriter) Write(p []byte) (n int, err error) { 42 | if w.errOnWrite { 43 | return w.writeNum, errorx.Raw("write error") 44 | } 45 | 46 | if w.writeNum > 0 { 47 | return w.writeNum, nil 48 | } 49 | return len(p), nil 50 | } 51 | 52 | func TestBufIOWriter_Close_error(t *testing.T) { 53 | bw := bufwrite.NewBufIOWriterSize(&closeWriter{errOnWrite: true}, 24) 54 | _, err := bw.WriteString("hi") 55 | assert.NoErr(t, err) 56 | 57 | // flush write error 58 | err = bw.Close() 59 | assert.Err(t, err) 60 | assert.Eq(t, "write error", err.Error()) 61 | 62 | bw = bufwrite.NewBufIOWriterSize(&closeWriter{errOnClose: true}, 24) 63 | 64 | // close error 65 | err = bw.Close() 66 | assert.Err(t, err) 67 | assert.Eq(t, "close error", err.Error()) 68 | } 69 | 70 | func TestBufIOWriter_Sync(t *testing.T) { 71 | w := new(bytes.Buffer) 72 | bw := bufwrite.NewBufIOWriter(w) 73 | 74 | _, err := bw.WriteString("hello") 75 | assert.NoErr(t, err) 76 | assert.Eq(t, 0, w.Len()) 77 | assert.Eq(t, "", w.String()) 78 | 79 | assert.NoErr(t, bw.Sync()) 80 | assert.Eq(t, "hello", w.String()) 81 | } 82 | 83 | func TestNewLineWriter(t *testing.T) { 84 | w := new(bytes.Buffer) 85 | bw := bufwrite.NewLineWriter(w) 86 | 87 | assert.True(t, bw.Size() > 0) 88 | assert.NoErr(t, bw.Flush()) 89 | 90 | _, err := bw.WriteString("hello") 91 | assert.NoErr(t, err) 92 | assert.Eq(t, "", w.String()) 93 | 94 | assert.NoErr(t, bw.Sync()) 95 | assert.Eq(t, "hello", w.String()) 96 | 97 | bw.Reset(w) 98 | } 99 | 100 | func TestLineWriter_Write_error(t *testing.T) { 101 | w := &closeWriter{errOnWrite: true} 102 | bw := bufwrite.NewLineWriterSize(w, 6) 103 | 104 | t.Run("flush err on write", func(t *testing.T) { 105 | w1 := &closeWriter{} 106 | bw.Reset(w1) 107 | n, err := bw.WriteString("hi") // write ok 108 | assert.NoErr(t, err) 109 | assert.Equal(t, 2, n) 110 | 111 | // fire flush 112 | w1.errOnWrite = true 113 | _, err = bw.WriteString("hello, tom") 114 | assert.Err(t, err) 115 | assert.Eq(t, "write error", err.Error()) 116 | }) 117 | 118 | _, err := bw.WriteString("hello, tom") 119 | assert.Err(t, err) 120 | assert.Eq(t, "write error", err.Error()) 121 | 122 | // get old error 123 | w.errOnWrite = false 124 | 125 | _, err = bw.WriteString("hello, wo") 126 | assert.Err(t, err) 127 | assert.Eq(t, "write error", err.Error()) 128 | 129 | bw.Reset(w) 130 | _, err = bw.WriteString("hello") 131 | assert.NoErr(t, err) 132 | } 133 | 134 | func TestLineWriter_Flush_error(t *testing.T) { 135 | t.Run("write ok but n < b.n", func(t *testing.T) { 136 | w := &closeWriter{} 137 | bw := bufwrite.NewLineWriterSize(w, 6) 138 | _, err := bw.WriteString("hi!") 139 | assert.NoErr(t, err) 140 | 141 | // err: write n < b.n 142 | w.writeNum = 1 143 | err = bw.Flush() 144 | assert.Err(t, err) 145 | assert.Eq(t, "short write", err.Error()) 146 | }) 147 | 148 | t.Run("write err and n < b.n", func(t *testing.T) { 149 | w := &closeWriter{} 150 | bw := bufwrite.NewLineWriterSize(w, 6) 151 | _, err := bw.WriteString("hi!") 152 | assert.NoErr(t, err) 153 | 154 | // err: write n < b.n 155 | w.writeNum = 1 156 | w.errOnWrite = true 157 | err = bw.Flush() 158 | assert.Err(t, err) 159 | assert.Eq(t, "write error", err.Error()) 160 | }) 161 | 162 | w := &closeWriter{} 163 | bw := bufwrite.NewLineWriterSize(w, 6) 164 | 165 | _, err := bw.WriteString("hello") 166 | assert.NoErr(t, err) 167 | // error on flush 168 | w.errOnWrite = true 169 | err = bw.Flush() 170 | assert.Err(t, err) 171 | assert.Eq(t, "write error", err.Error()) 172 | 173 | // err: write n < b.n 174 | w.writeNum = 2 175 | err = bw.Flush() 176 | assert.Err(t, err) 177 | w.writeNum = 0 178 | 179 | // get old error 180 | w.errOnWrite = false 181 | err = bw.Flush() 182 | assert.Err(t, err) 183 | assert.Eq(t, "write error", err.Error()) 184 | 185 | bw.Reset(w) 186 | _, err = bw.WriteString("hello") 187 | assert.NoErr(t, err) 188 | } 189 | 190 | func TestLineWriter_Close_error(t *testing.T) { 191 | w := &closeWriter{} 192 | bw := bufwrite.NewLineWriterSize(w, 8) 193 | 194 | _, err := bw.WriteString("hello") 195 | assert.NoErr(t, err) 196 | 197 | // error on flush 198 | w.errOnWrite = true 199 | err = bw.Close() 200 | assert.Err(t, err) 201 | assert.Eq(t, "write error", err.Error()) 202 | 203 | w = &closeWriter{errOnClose: true} 204 | bw = bufwrite.NewLineWriterSize(w, 8) 205 | 206 | err = bw.Close() 207 | assert.Err(t, err) 208 | assert.Eq(t, "close error", err.Error()) 209 | } 210 | 211 | func TestNewLineWriterSize(t *testing.T) { 212 | w := new(bytes.Buffer) 213 | bw := bufwrite.NewLineWriterSize(w, 12) 214 | 215 | _, err := bw.WriteString("hello, ") 216 | assert.NoErr(t, err) 217 | assert.Eq(t, 0, w.Len()) 218 | assert.True(t, bw.Size() > 0) 219 | 220 | _, err = bw.WriteString("worlds. oh") 221 | assert.NoErr(t, err) 222 | assert.Eq(t, "hello, worlds. oh", w.String()) // different the BufIOWriter 223 | 224 | _, err = bw.WriteString("...") 225 | assert.NoErr(t, err) 226 | assert.NoErr(t, bw.Close()) 227 | assert.Eq(t, "hello, worlds. oh...", w.String()) 228 | w.Reset() 229 | 230 | bw = bufwrite.NewLineWriterSize(bw, 8) 231 | assert.Eq(t, 12, bw.Size()) 232 | 233 | bw = bufwrite.NewLineWriterSize(w, -12) 234 | assert.True(t, bw.Size() > 12) 235 | } 236 | -------------------------------------------------------------------------------- /bufwrite/line_writer.go: -------------------------------------------------------------------------------- 1 | package bufwrite 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | const ( 8 | defaultBufSize = 1024 * 8 9 | ) 10 | 11 | // LineWriter implements buffering for an io.Writer object. 12 | // If an error occurs writing to a LineWriter, no more data will be 13 | // accepted and all subsequent writes, and Flush, will return the error. 14 | // After all data has been written, the client should call the 15 | // Flush method to guarantee all data has been forwarded to 16 | // the underlying io.Writer. 17 | // 18 | // from bufio.Writer. 19 | // 20 | // Change: 21 | // 22 | // always keep write full line. more difference please see Write 23 | type LineWriter struct { 24 | err error 25 | buf []byte 26 | n int 27 | wr io.Writer 28 | } 29 | 30 | // NewLineWriterSize returns a new LineWriter whose buffer has at least the specified 31 | // size. If the argument io.Writer is already a LineWriter with large enough 32 | // size, it returns the underlying LineWriter. 33 | func NewLineWriterSize(w io.Writer, size int) *LineWriter { 34 | // Is it already a LineWriter? 35 | b, ok := w.(*LineWriter) 36 | if ok && len(b.buf) >= size { 37 | return b 38 | } 39 | if size <= 0 { 40 | size = defaultBufSize 41 | } 42 | 43 | return &LineWriter{ 44 | buf: make([]byte, size), 45 | wr: w, 46 | } 47 | } 48 | 49 | // NewLineWriter returns a new LineWriter whose buffer has the default size. 50 | func NewLineWriter(w io.Writer) *LineWriter { 51 | return NewLineWriterSize(w, defaultBufSize) 52 | } 53 | 54 | // Size returns the size of the underlying buffer in bytes. 55 | func (b *LineWriter) Size() int { return len(b.buf) } 56 | 57 | // Reset discards any un-flushed buffered data, clears any error, and 58 | // resets b to write its output to w. 59 | func (b *LineWriter) Reset(w io.Writer) { 60 | b.n = 0 61 | b.wr = w 62 | b.err = nil 63 | b.buf = b.buf[:0] 64 | } 65 | 66 | // Close implements the io.Closer 67 | func (b *LineWriter) Close() error { 68 | if err := b.Flush(); err != nil { 69 | return err 70 | } 71 | 72 | // is closer 73 | if c, ok := b.wr.(io.Closer); ok { 74 | return c.Close() 75 | } 76 | return nil 77 | } 78 | 79 | // Sync implements the Syncer 80 | func (b *LineWriter) Sync() error { 81 | return b.Flush() 82 | } 83 | 84 | // Flush writes any buffered data to the underlying io.Writer. 85 | // 86 | // TIP: please add lock before call the method. 87 | func (b *LineWriter) Flush() error { 88 | if b.err != nil { 89 | return b.err 90 | } 91 | if b.n == 0 { 92 | return nil 93 | } 94 | 95 | n, err := b.wr.Write(b.buf[0:b.n]) 96 | if n < b.n && err == nil { 97 | err = io.ErrShortWrite 98 | } 99 | if err != nil { 100 | if n > 0 && n < b.n { 101 | copy(b.buf[0:b.n-n], b.buf[n:b.n]) 102 | } 103 | b.n -= n 104 | b.err = err 105 | return err 106 | } 107 | 108 | b.n = 0 109 | return nil 110 | } 111 | 112 | // Available returns how many bytes are unused in the buffer. 113 | func (b *LineWriter) Available() int { return len(b.buf) - b.n } 114 | 115 | // Buffered returns the number of bytes that have been written into the current buffer. 116 | func (b *LineWriter) Buffered() int { return b.n } 117 | 118 | // Write writes the contents of p into the buffer. 119 | // It returns the number of bytes written. 120 | // If nn < len(p), it also returns an error explaining 121 | // why the write is short. 122 | func (b *LineWriter) Write(p []byte) (nn int, err error) { 123 | // 原来的会造成 p 写了一部分到 b.wr, 还有一部分在 b.buf, 124 | // 如果现在外部工具从 b.wr 收集数据,会收集到一行无法解析的数据(例如每个p是一行json日志) 125 | // for len(p) > b.Available() && b.err == nil { 126 | // var n int 127 | // if b.Buffered() == 0 { 128 | // // Large write, empty buffer. 129 | // // Write directly from p to avoid copy. 130 | // n, b.err = b.wr.Write(p) 131 | // } else { 132 | // n = copy(b.buf[b.n:], p) 133 | // b.n += n 134 | // b.Flush() 135 | // } 136 | // nn += n 137 | // p = p[n:] 138 | // } 139 | 140 | // UP: 改造一下逻辑,如果 len(p) > b.Available() 就将buf 和 p 都写入 b.wr 141 | if len(p) > b.Available() && b.err == nil { 142 | nn = b.Buffered() 143 | if nn > 0 { 144 | _ = b.Flush() 145 | if b.err != nil { 146 | return nn, b.err 147 | } 148 | } 149 | 150 | var n int 151 | n, b.err = b.wr.Write(p) 152 | if b.err != nil { 153 | return nn, b.err 154 | } 155 | 156 | nn += n 157 | return nn, nil 158 | } 159 | 160 | if b.err != nil { 161 | return nn, b.err 162 | } 163 | 164 | n := copy(b.buf[b.n:], p) 165 | b.n += n 166 | nn += n 167 | return nn, nil 168 | } 169 | 170 | // WriteString to writer 171 | func (b *LineWriter) WriteString(s string) (int, error) { 172 | return b.Write([]byte(s)) 173 | } 174 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package slog_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/byteutil" 9 | "github.com/gookit/goutil/dump" 10 | "github.com/gookit/goutil/errorx" 11 | "github.com/gookit/goutil/testutil/assert" 12 | "github.com/gookit/gsr" 13 | "github.com/gookit/slog" 14 | "github.com/gookit/slog/handler" 15 | ) 16 | 17 | var ( 18 | testData1 = slog.M{"key0": "val0", "age": 23} 19 | // testData2 = slog.M{"key0": "val0", "age": 23, "sub": slog.M{ 20 | // "subKey0": 345, 21 | // }} 22 | ) 23 | 24 | func TestDefine_basic(t *testing.T) { 25 | assert.NotEmpty(t, slog.NoTimeFields) 26 | assert.NotEmpty(t, slog.FieldKeyDate) 27 | assert.NotEmpty(t, slog.FieldKeyTime) 28 | assert.NotEmpty(t, slog.FieldKeyCaller) 29 | assert.NotEmpty(t, slog.FieldKeyError) 30 | } 31 | 32 | func TestM_String(t *testing.T) { 33 | m := slog.M{ 34 | "k0": 12, 35 | "k1": "abc", 36 | "k2": true, 37 | "k3": 23.45, 38 | "k4": []int{12, 23}, 39 | "k5": []string{"ab", "bc"}, 40 | "k6": map[string]any{ 41 | "k6-1": 23, 42 | "k6-2": "def", 43 | }, 44 | } 45 | 46 | fmt.Println(m) 47 | dump.P(m.String(), m) 48 | assert.NotEmpty(t, m.String()) 49 | } 50 | 51 | func TestLevel_Name(t *testing.T) { 52 | assert.Eq(t, "INFO", slog.InfoLevel.Name()) 53 | assert.Eq(t, "INFO", slog.InfoLevel.String()) 54 | assert.Eq(t, "info", slog.InfoLevel.LowerName()) 55 | assert.Eq(t, "unknown", slog.Level(330).LowerName()) 56 | } 57 | 58 | func TestLevelByName(t *testing.T) { 59 | assert.Eq(t, slog.InfoLevel, slog.LevelByName("info")) 60 | assert.Eq(t, slog.InfoLevel, slog.LevelByName("invalid")) 61 | } 62 | 63 | func TestLevelName(t *testing.T) { 64 | for level, wantName := range slog.LevelNames { 65 | realName := slog.LevelName(level) 66 | assert.Eq(t, wantName, realName) 67 | } 68 | 69 | assert.Eq(t, "UNKNOWN", slog.LevelName(20)) 70 | } 71 | 72 | func TestLevel_ShouldHandling(t *testing.T) { 73 | assert.True(t, slog.InfoLevel.ShouldHandling(slog.ErrorLevel)) 74 | assert.False(t, slog.InfoLevel.ShouldHandling(slog.TraceLevel)) 75 | 76 | assert.True(t, slog.DebugLevel.ShouldHandling(slog.InfoLevel)) 77 | assert.False(t, slog.DebugLevel.ShouldHandling(slog.TraceLevel)) 78 | } 79 | 80 | func TestLevels_Contains(t *testing.T) { 81 | assert.True(t, slog.DangerLevels.Contains(slog.ErrorLevel)) 82 | assert.False(t, slog.DangerLevels.Contains(slog.InfoLevel)) 83 | assert.True(t, slog.NormalLevels.Contains(slog.InfoLevel)) 84 | assert.False(t, slog.NormalLevels.Contains(slog.PanicLevel)) 85 | } 86 | 87 | func newLogRecord(msg string) *slog.Record { 88 | r := &slog.Record{ 89 | Channel: slog.DefaultChannelName, 90 | Level: slog.InfoLevel, 91 | Message: msg, 92 | Time: slog.DefaultClockFn.Now(), 93 | Data: map[string]any{ 94 | "data_key0": "value", 95 | "username": "inhere", 96 | }, 97 | Extra: map[string]any{ 98 | "source": "linux", 99 | "extra_key0": "hello", 100 | }, 101 | // Caller: goinfo.GetCallerInfo(), 102 | } 103 | 104 | r.Init(true) 105 | return r 106 | } 107 | 108 | type closedBuffer struct { 109 | bytes.Buffer 110 | } 111 | 112 | func newBuffer() *closedBuffer { 113 | return &closedBuffer{} 114 | } 115 | 116 | func (w *closedBuffer) Close() error { 117 | return nil 118 | } 119 | 120 | func (w *closedBuffer) StringReset() string { 121 | s := w.Buffer.String() 122 | w.Reset() 123 | return s 124 | } 125 | 126 | type testHandler struct { 127 | slog.FormatterWrapper 128 | byteutil.Buffer 129 | errOnHandle bool 130 | errOnClose bool 131 | errOnFlush bool 132 | // hooks 133 | callOnFlush func() 134 | // tip: 设置为true,默认会让 error,fatal 等信息提前被reset丢弃掉. 135 | // see Logger.writeRecord() 136 | resetOnFlush bool 137 | } 138 | 139 | // built in test, will collect logs to buffer 140 | func newTestHandler() *testHandler { 141 | return &testHandler{resetOnFlush: true} 142 | } 143 | 144 | func (h *testHandler) IsHandling(_ slog.Level) bool { 145 | return true 146 | } 147 | 148 | func (h *testHandler) Close() error { 149 | if h.errOnClose { 150 | return errorx.Raw("close error") 151 | } 152 | 153 | h.Reset() 154 | return nil 155 | } 156 | 157 | func (h *testHandler) Flush() error { 158 | if h.errOnFlush { 159 | return errorx.Raw("flush error") 160 | } 161 | if h.callOnFlush != nil { 162 | h.callOnFlush() 163 | } 164 | 165 | if h.resetOnFlush { 166 | h.Reset() 167 | } 168 | return nil 169 | } 170 | 171 | func (h *testHandler) Handle(r *slog.Record) error { 172 | if h.errOnHandle { 173 | return errorx.Raw("handle error") 174 | } 175 | 176 | bs, err := h.Format(r) 177 | if err != nil { 178 | return err 179 | } 180 | h.Write(bs) 181 | return nil 182 | } 183 | 184 | type testFormatter struct { 185 | errOnFormat bool 186 | } 187 | 188 | func newTestFormatter(errOnFormat ...bool) *testFormatter { 189 | return &testFormatter{ 190 | errOnFormat: len(errOnFormat) > 0 && errOnFormat[0], 191 | } 192 | } 193 | 194 | func (f testFormatter) Format(r *slog.Record) ([]byte, error) { 195 | if f.errOnFormat { 196 | return nil, errorx.Raw("format error") 197 | } 198 | return []byte(r.Message), nil 199 | } 200 | 201 | func newLogger() *slog.Logger { 202 | return slog.NewWithConfig(func(l *slog.Logger) { 203 | l.ReportCaller = true 204 | l.DoNothingOnPanicFatal() 205 | }) 206 | } 207 | 208 | // newTestLogger create a logger for test, will write logs to buffer 209 | func newTestLogger() (*closedBuffer, *slog.Logger) { 210 | l := slog.NewWithConfig(func(l *slog.Logger) { 211 | l.DoNothingOnPanicFatal() 212 | l.CallerFlag = slog.CallerFlagFull 213 | }) 214 | w := newBuffer() 215 | h := handler.NewIOWriter(w, slog.AllLevels) 216 | // fmt.Print("Template:", h.TextFormatter().Template()) 217 | l.SetHandlers([]slog.Handler{h}) 218 | return w, l 219 | } 220 | 221 | func printAllLevelLogs(l gsr.Logger, args ...any) { 222 | l.Debug(args...) 223 | l.Info(args...) 224 | l.Warn(args...) 225 | l.Error(args...) 226 | l.Print(args...) 227 | l.Println(args...) 228 | l.Fatal(args...) 229 | l.Fatalln(args...) 230 | l.Panic(args...) 231 | l.Panicln(args...) 232 | 233 | sl, ok := l.(*slog.Logger) 234 | if ok { 235 | sl.Trace(args...) 236 | sl.Notice(args...) 237 | sl.ErrorT(errorx.Raw("a error object")) 238 | sl.ErrorT(errorx.New("error with stack info")) 239 | } 240 | } 241 | 242 | func printfAllLevelLogs(l gsr.Logger, tpl string, args ...any) { 243 | l.Printf(tpl, args...) 244 | l.Debugf(tpl, args...) 245 | l.Infof(tpl, args...) 246 | l.Warnf(tpl, args...) 247 | l.Errorf(tpl, args...) 248 | l.Panicf(tpl, args...) 249 | l.Fatalf(tpl, args...) 250 | 251 | if sl, ok := l.(*slog.Logger); ok { 252 | sl.Noticef(tpl, args...) 253 | sl.Tracef(tpl, args...) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package slog_test 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/gookit/slog" 9 | "github.com/gookit/slog/handler" 10 | ) 11 | 12 | func Example_quickStart() { 13 | slog.Info("info log message") 14 | slog.Warn("warning log message") 15 | slog.Infof("info log %s", "message") 16 | slog.Debugf("debug %s", "message") 17 | } 18 | 19 | func Example_configSlog() { 20 | slog.Configure(func(logger *slog.SugaredLogger) { 21 | f := logger.Formatter.(*slog.TextFormatter) 22 | f.EnableColor = true 23 | }) 24 | 25 | slog.Trace("this is a simple log message") 26 | slog.Debug("this is a simple log message") 27 | slog.Info("this is a simple log message") 28 | slog.Notice("this is a simple log message") 29 | slog.Warn("this is a simple log message") 30 | slog.Error("this is a simple log message") 31 | slog.Fatal("this is a simple log message") 32 | } 33 | 34 | func Example_useJSONFormat() { 35 | // use JSON formatter 36 | slog.SetFormatter(slog.NewJSONFormatter()) 37 | 38 | slog.Info("info log message") 39 | slog.Warn("warning log message") 40 | slog.WithData(slog.M{ 41 | "key0": 134, 42 | "key1": "abc", 43 | }).Infof("info log %s", "message") 44 | 45 | r := slog.WithFields(slog.M{ 46 | "category": "service", 47 | "IP": "127.0.0.1", 48 | }) 49 | r.Infof("info %s", "message") 50 | r.Debugf("debug %s", "message") 51 | } 52 | 53 | func ExampleNew() { 54 | mylog := slog.New() 55 | levels := slog.AllLevels 56 | 57 | mylog.AddHandler(handler.MustFileHandler("app.log", handler.WithLogLevels(levels))) 58 | 59 | mylog.Info("info log message") 60 | mylog.Warn("warning log message") 61 | mylog.Infof("info log %s", "message") 62 | } 63 | 64 | func ExampleFlushDaemon() { 65 | wg := sync.WaitGroup{} 66 | wg.Add(1) 67 | 68 | go slog.FlushDaemon(func() { 69 | fmt.Println("flush daemon stopped") 70 | slog.MustClose() 71 | wg.Done() 72 | }) 73 | 74 | go func() { 75 | // mock app running 76 | time.Sleep(time.Second * 2) 77 | 78 | // stop daemon 79 | fmt.Println("stop flush daemon") 80 | slog.StopDaemon() 81 | }() 82 | 83 | // wait for stop 84 | wg.Wait() 85 | } 86 | -------------------------------------------------------------------------------- /formatter.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import "runtime" 4 | 5 | // 6 | // Formatter interface 7 | // 8 | 9 | // Formatter interface 10 | type Formatter interface { 11 | // Format you can format record and write result to record.Buffer 12 | Format(record *Record) ([]byte, error) 13 | } 14 | 15 | // FormatterFunc wrapper definition 16 | type FormatterFunc func(r *Record) ([]byte, error) 17 | 18 | // Format a log record 19 | func (fn FormatterFunc) Format(r *Record) ([]byte, error) { 20 | return fn(r) 21 | } 22 | 23 | // Formattable interface 24 | type Formattable interface { 25 | // Formatter get the log formatter 26 | Formatter() Formatter 27 | // SetFormatter set the log formatter 28 | SetFormatter(Formatter) 29 | } 30 | 31 | // FormattableTrait alias of FormatterWrapper 32 | type FormattableTrait = FormatterWrapper 33 | 34 | // FormatterWrapper use for format log record. 35 | // 36 | // Default will use the TextFormatter 37 | type FormatterWrapper struct { 38 | // if not set, default uses the TextFormatter 39 | formatter Formatter 40 | } 41 | 42 | // Formatter get formatter. if not set, will return TextFormatter 43 | func (f *FormatterWrapper) Formatter() Formatter { 44 | if f.formatter == nil { 45 | f.formatter = NewTextFormatter() 46 | } 47 | return f.formatter 48 | } 49 | 50 | // SetFormatter to handler 51 | func (f *FormatterWrapper) SetFormatter(formatter Formatter) { 52 | f.formatter = formatter 53 | } 54 | 55 | // Format log record to bytes 56 | func (f *FormatterWrapper) Format(record *Record) ([]byte, error) { 57 | return f.Formatter().Format(record) 58 | } 59 | 60 | // CallerFormatFn caller format func 61 | type CallerFormatFn func(rf *runtime.Frame) (cs string) 62 | 63 | // AsTextFormatter util func 64 | func AsTextFormatter(f Formatter) *TextFormatter { 65 | if tf, ok := f.(*TextFormatter); ok { 66 | return tf 67 | } 68 | panic("slog: cannot cast input as *TextFormatter") 69 | } 70 | 71 | // AsJSONFormatter util func 72 | func AsJSONFormatter(f Formatter) *JSONFormatter { 73 | if jf, ok := f.(*JSONFormatter); ok { 74 | return jf 75 | } 76 | panic("slog: cannot cast input as *JSONFormatter") 77 | } 78 | -------------------------------------------------------------------------------- /formatter_json.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/valyala/bytebufferpool" 7 | ) 8 | 9 | var ( 10 | // DefaultFields default log export fields for json formatter. 11 | DefaultFields = []string{ 12 | FieldKeyDatetime, 13 | FieldKeyChannel, 14 | FieldKeyLevel, 15 | FieldKeyCaller, 16 | FieldKeyMessage, 17 | FieldKeyData, 18 | FieldKeyExtra, 19 | } 20 | 21 | // NoTimeFields log export fields without time 22 | NoTimeFields = []string{ 23 | FieldKeyChannel, 24 | FieldKeyLevel, 25 | FieldKeyMessage, 26 | FieldKeyData, 27 | FieldKeyExtra, 28 | } 29 | ) 30 | 31 | // JSONFormatter definition 32 | type JSONFormatter struct { 33 | // Fields exported log fields. default is DefaultFields 34 | Fields []string 35 | // Aliases for output fields. you can change the export field name. 36 | // 37 | // - item: `"field" : "output name"` 38 | // 39 | // eg: {"message": "msg"} export field will display "msg" 40 | Aliases StringMap 41 | 42 | // PrettyPrint will indent all JSON logs 43 | PrettyPrint bool 44 | // TimeFormat the time format layout. default is DefaultTimeFormat 45 | TimeFormat string 46 | // CallerFormatFunc the caller format layout. default is defined by CallerFlag 47 | CallerFormatFunc CallerFormatFn 48 | } 49 | 50 | // NewJSONFormatter create new JSONFormatter 51 | func NewJSONFormatter(fn ...func(f *JSONFormatter)) *JSONFormatter { 52 | f := &JSONFormatter{ 53 | // Aliases: make(StringMap, 0), 54 | Fields: DefaultFields, 55 | TimeFormat: DefaultTimeFormat, 56 | } 57 | 58 | if len(fn) > 0 { 59 | fn[0](f) 60 | } 61 | return f 62 | } 63 | 64 | // Configure current formatter 65 | func (f *JSONFormatter) Configure(fn func(*JSONFormatter)) *JSONFormatter { 66 | fn(f) 67 | return f 68 | } 69 | 70 | // AddField for export 71 | func (f *JSONFormatter) AddField(name string) *JSONFormatter { 72 | f.Fields = append(f.Fields, name) 73 | return f 74 | } 75 | 76 | var jsonPool bytebufferpool.Pool 77 | 78 | // Format a log record to JSON bytes 79 | func (f *JSONFormatter) Format(r *Record) ([]byte, error) { 80 | logData := make(M, len(f.Fields)) 81 | 82 | // TODO perf: use buf write build JSON string. 83 | for _, field := range f.Fields { 84 | outName, ok := f.Aliases[field] 85 | if !ok { 86 | outName = field 87 | } 88 | 89 | switch { 90 | case field == FieldKeyDatetime: 91 | logData[outName] = r.Time.Format(f.TimeFormat) 92 | case field == FieldKeyTimestamp: 93 | logData[outName] = r.timestamp() 94 | case field == FieldKeyCaller && r.Caller != nil: 95 | logData[outName] = formatCaller(r.Caller, r.CallerFlag, f.CallerFormatFunc) 96 | case field == FieldKeyLevel: 97 | logData[outName] = r.LevelName() 98 | case field == FieldKeyChannel: 99 | logData[outName] = r.Channel 100 | case field == FieldKeyMessage: 101 | logData[outName] = r.Message 102 | case field == FieldKeyData: 103 | logData[outName] = r.Data 104 | case field == FieldKeyExtra: 105 | logData[outName] = r.Extra 106 | // default: 107 | // logData[outName] = r.Fields[field] 108 | } 109 | } 110 | 111 | // exported custom fields 112 | for field, value := range r.Fields { 113 | fieldKey := field 114 | if _, has := logData[field]; has { 115 | fieldKey = "fields." + field 116 | } 117 | 118 | logData[fieldKey] = value 119 | } 120 | 121 | // sort.Interface() 122 | buf := jsonPool.Get() 123 | // buf.Reset() 124 | defer jsonPool.Put(buf) 125 | // buf := r.NewBuffer() 126 | // buf.Reset() 127 | // buf.Grow(256) 128 | 129 | encoder := json.NewEncoder(buf) 130 | if f.PrettyPrint { 131 | encoder.SetIndent("", " ") 132 | } 133 | 134 | // has been added newline in Encode(). 135 | err := encoder.Encode(logData) 136 | return buf.Bytes(), err 137 | } 138 | -------------------------------------------------------------------------------- /formatter_test.go: -------------------------------------------------------------------------------- 1 | package slog_test 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/gookit/goutil/byteutil" 10 | "github.com/gookit/goutil/dump" 11 | "github.com/gookit/goutil/testutil/assert" 12 | "github.com/gookit/slog" 13 | "github.com/gookit/slog/handler" 14 | ) 15 | 16 | func TestFormattableTrait_Formatter(t *testing.T) { 17 | ft := &slog.FormattableTrait{} 18 | tf := slog.AsTextFormatter(ft.Formatter()) 19 | assert.NotNil(t, tf) 20 | assert.Panics(t, func() { 21 | slog.AsJSONFormatter(ft.Formatter()) 22 | }) 23 | 24 | ft.SetFormatter(slog.NewJSONFormatter()) 25 | jf := slog.AsJSONFormatter(ft.Formatter()) 26 | assert.NotNil(t, jf) 27 | assert.Panics(t, func() { 28 | slog.AsTextFormatter(ft.Formatter()) 29 | }) 30 | } 31 | 32 | func TestFormattable_Format(t *testing.T) { 33 | r := newLogRecord("TEST_LOG_MESSAGE format") 34 | f := &slog.FormattableTrait{} 35 | assert.Eq(t, "slog: TEST_LOG_MESSAGE format", r.GoString()) 36 | 37 | bts, err := f.Format(r) 38 | assert.NoErr(t, err) 39 | 40 | str := string(bts) 41 | assert.Contains(t, str, "TEST_LOG_MESSAGE format") 42 | 43 | fn := slog.FormatterFunc(func(r *slog.Record) ([]byte, error) { 44 | return []byte(r.Message), nil 45 | }) 46 | 47 | bts, err = fn.Format(r) 48 | assert.NoErr(t, err) 49 | 50 | str = string(bts) 51 | assert.Contains(t, str, "TEST_LOG_MESSAGE format") 52 | } 53 | 54 | func TestNewTextFormatter(t *testing.T) { 55 | f := slog.NewTextFormatter() 56 | 57 | dump.Println(f.Fields()) 58 | assert.Contains(t, f.Fields(), "datetime") 59 | assert.Len(t, f.Fields(), strings.Count(slog.DefaultTemplate, "{{")) 60 | 61 | f.SetTemplate(slog.NamedTemplate) 62 | dump.Println(f.Fields()) 63 | assert.Contains(t, f.Fields(), "datetime") 64 | assert.Len(t, f.Fields(), strings.Count(slog.NamedTemplate, "{{")) 65 | 66 | f.WithEnableColor(true) 67 | assert.True(t, f.EnableColor) 68 | 69 | t.Run("CallerFormatFunc", func(t *testing.T) { 70 | buf := byteutil.NewBuffer() 71 | h := handler.IOWriterWithMaxLevel(buf, slog.DebugLevel) 72 | h.SetFormatter(slog.TextFormatterWith(func(f *slog.TextFormatter) { 73 | f.CallerFormatFunc = func(rf *runtime.Frame) string { 74 | return "custom_caller" 75 | } 76 | })) 77 | 78 | l := slog.NewWithHandlers(h) 79 | l.Debug("test message") 80 | assert.Contains(t, buf.String(), "custom_caller") 81 | }) 82 | 83 | f1 := slog.NewTextFormatter() 84 | f1.Configure(func(f *slog.TextFormatter) { 85 | f.FullDisplay = true 86 | }) 87 | assert.True(t, f1.FullDisplay) 88 | } 89 | 90 | func TestTextFormatter_Format(t *testing.T) { 91 | r := newLogRecord("TEST_LOG_MESSAGE") 92 | f := slog.NewTextFormatter() 93 | 94 | bs, err := f.Format(r) 95 | logTxt := string(bs) 96 | fmt.Println(f.Template(), logTxt) 97 | 98 | assert.NoErr(t, err) 99 | assert.NotEmpty(t, logTxt) 100 | assert.NotContains(t, logTxt, "{{") 101 | assert.NotContains(t, logTxt, "}}") 102 | } 103 | 104 | func TestTextFormatter_ColorRenderFunc(t *testing.T) { 105 | f := slog.NewTextFormatter() 106 | f.WithEnableColor(true) 107 | f.ColorRenderFunc = func(field, s string, l slog.Level) string { 108 | return fmt.Sprintf("NO-%s-NO", s) 109 | } 110 | 111 | r := newLogRecord("TEST_LOG_MESSAGE") 112 | bts, err := f.Format(r) 113 | assert.NoErr(t, err) 114 | str := string(bts) 115 | assert.StrContains(t, str, "[NO-info-NO]") 116 | assert.StrContains(t, str, "NO-TEST_LOG_MESSAGE-NO") 117 | } 118 | 119 | func TestTextFormatter_LimitLevelNameLen(t *testing.T) { 120 | f := slog.TextFormatterWith(slog.LimitLevelNameLen(4)) 121 | 122 | h := handler.ConsoleWithMaxLevel(slog.TraceLevel) 123 | h.SetFormatter(f) 124 | 125 | th := newTestHandler() 126 | th.resetOnFlush = false 127 | th.SetFormatter(f) 128 | 129 | l := slog.NewWithHandlers(h, th) 130 | l.DoNothingOnPanicFatal() 131 | 132 | for _, level := range slog.AllLevels { 133 | l.Logf(level, "a %s test message", level.String()) 134 | } 135 | assert.NoErr(t, l.LastErr()) 136 | 137 | str := th.ResetAndGet() 138 | assert.StrContains(t, str, "[PANI]") 139 | assert.StrContains(t, str, "[FATA]") 140 | assert.StrContains(t, str, "[ERRO]") 141 | assert.StrContains(t, str, "[TRAC]") 142 | } 143 | 144 | func TestTextFormatter_LimitLevelNameLen2(t *testing.T) { 145 | // set to max length. 146 | f := slog.TextFormatterWith(slog.LimitLevelNameLen(7)) 147 | 148 | h := handler.ConsoleWithMaxLevel(slog.TraceLevel) 149 | h.SetFormatter(f) 150 | 151 | th := newTestHandler() 152 | th.resetOnFlush = false 153 | th.SetFormatter(f) 154 | 155 | l := slog.NewWithHandlers(h, th) 156 | l.DoNothingOnPanicFatal() 157 | 158 | for _, level := range slog.AllLevels { 159 | l.Logf(level, "a %s test message", level.String()) 160 | } 161 | assert.NoErr(t, l.LastErr()) 162 | 163 | str := th.ResetAndGet() 164 | assert.StrContains(t, str, "[PANIC ]") 165 | assert.StrContains(t, str, "[FATAL ]") 166 | assert.StrContains(t, str, "[ERROR ]") 167 | assert.StrContains(t, str, "[WARNING]") 168 | } 169 | 170 | func TestNewJSONFormatter(t *testing.T) { 171 | f := slog.NewJSONFormatter() 172 | f.AddField(slog.FieldKeyTimestamp) 173 | 174 | h := handler.ConsoleWithLevels(slog.AllLevels) 175 | h.SetFormatter(f) 176 | 177 | l := slog.NewWithHandlers(h) 178 | 179 | fields := slog.M{ 180 | "field1": 123, 181 | "field2": "abc", 182 | "message": "field name is same of message", // will be as fields.message 183 | } 184 | 185 | l.WithFields(fields).Info("info", "message") 186 | 187 | t.Run("CallerFormatFunc", func(t *testing.T) { 188 | h.SetFormatter(slog.NewJSONFormatter(func(f *slog.JSONFormatter) { 189 | f.CallerFormatFunc = func(rf *runtime.Frame) string { 190 | return rf.Function 191 | } 192 | })) 193 | l.WithFields(fields).Info("info", "message") 194 | }) 195 | 196 | // PrettyPrint=true 197 | t.Run("PrettyPrint", func(t *testing.T) { 198 | l = slog.New() 199 | h = handler.ConsoleWithMaxLevel(slog.DebugLevel) 200 | f = slog.NewJSONFormatter(func(f *slog.JSONFormatter) { 201 | f.Aliases = slog.StringMap{ 202 | "level": "levelName", 203 | } 204 | f.PrettyPrint = true 205 | }) 206 | 207 | h.SetFormatter(f) 208 | 209 | l.AddHandler(h) 210 | l.WithFields(fields). 211 | SetData(slog.M{"key1": "val1"}). 212 | SetExtra(slog.M{"ext1": "val1"}). 213 | Info("info message and PrettyPrint is TRUE") 214 | 215 | }) 216 | } 217 | -------------------------------------------------------------------------------- /formatter_text.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import ( 4 | "github.com/gookit/color" 5 | "github.com/valyala/bytebufferpool" 6 | ) 7 | 8 | // there are built in text log template 9 | const ( 10 | DefaultTemplate = "[{{datetime}}] [{{channel}}] [{{level}}] [{{caller}}] {{message}} {{data}} {{extra}}\n" 11 | NamedTemplate = "{{datetime}} channel={{channel}} level={{level}} [file={{caller}}] message={{message}} data={{data}}\n" 12 | ) 13 | 14 | // ColorTheme for format log to console 15 | var ColorTheme = map[Level]color.Color{ 16 | PanicLevel: color.FgRed, 17 | FatalLevel: color.FgRed, 18 | ErrorLevel: color.FgMagenta, 19 | WarnLevel: color.FgYellow, 20 | NoticeLevel: color.OpBold, 21 | InfoLevel: color.FgGreen, 22 | DebugLevel: color.FgCyan, 23 | // TraceLevel: color.FgLightGreen, 24 | } 25 | 26 | // TextFormatter definition 27 | type TextFormatter struct { 28 | // template text template for render output log messages 29 | template string 30 | // fields list, parsed from template string. 31 | // 32 | // NOTE: contains no-field items in the list. eg: ["level", "}}"} 33 | fields []string 34 | 35 | // TimeFormat the time format layout. default is DefaultTimeFormat 36 | TimeFormat string 37 | // Enable color on print log to terminal 38 | EnableColor bool 39 | // ColorTheme setting on render color on terminal 40 | ColorTheme map[Level]color.Color 41 | // FullDisplay Whether to display when record.Data, record.Extra, etc. are empty 42 | FullDisplay bool 43 | // EncodeFunc data encode for Record.Data, Record.Extra, etc. 44 | // 45 | // Default is encode by EncodeToString() 46 | EncodeFunc func(v any) string 47 | // CallerFormatFunc the caller format layout. default is defined by CallerFlag 48 | CallerFormatFunc CallerFormatFn 49 | // LevelFormatFunc custom the level name format. 50 | LevelFormatFunc func(s string) string 51 | // ColorRenderFunc custom color render func. 52 | // 53 | // - `s`: level name or message 54 | ColorRenderFunc func(filed, s string, l Level) string 55 | 56 | // TODO BeforeFunc call it before format, update fields or other 57 | // BeforeFunc func(r *Record) 58 | } 59 | 60 | // TextFormatterFn definition 61 | type TextFormatterFn func(*TextFormatter) 62 | 63 | // NewTextFormatter create new TextFormatter 64 | func NewTextFormatter(template ...string) *TextFormatter { 65 | var fmtTpl string 66 | if len(template) > 0 { 67 | fmtTpl = template[0] 68 | } else { 69 | fmtTpl = DefaultTemplate 70 | } 71 | 72 | f := &TextFormatter{ 73 | // default options 74 | ColorTheme: ColorTheme, 75 | TimeFormat: DefaultTimeFormat, 76 | // EnableColor: color.SupportColor(), 77 | // EncodeFunc: func(v any) string { 78 | // return fmt.Sprint(v) 79 | // }, 80 | EncodeFunc: EncodeToString, 81 | } 82 | f.SetTemplate(fmtTpl) 83 | 84 | return f 85 | } 86 | 87 | // TextFormatterWith create new TextFormatter with options 88 | func TextFormatterWith(fns ...TextFormatterFn) *TextFormatter { 89 | return NewTextFormatter().WithOptions(fns...) 90 | } 91 | 92 | // LimitLevelNameLen limit the length of the level name 93 | func LimitLevelNameLen(length int) TextFormatterFn { 94 | return func(f *TextFormatter) { 95 | f.LevelFormatFunc = func(s string) string { 96 | return FormatLevelName(s, length) 97 | } 98 | } 99 | } 100 | 101 | // Configure the formatter 102 | func (f *TextFormatter) Configure(fn TextFormatterFn) *TextFormatter { 103 | return f.WithOptions(fn) 104 | } 105 | 106 | // WithOptions func on the formatter 107 | func (f *TextFormatter) WithOptions(fns ...TextFormatterFn) *TextFormatter { 108 | for _, fn := range fns { 109 | fn(f) 110 | } 111 | return f 112 | } 113 | 114 | // SetTemplate set the log format template and update field-map 115 | func (f *TextFormatter) SetTemplate(fmtTpl string) { 116 | f.template = fmtTpl 117 | f.fields = parseTemplateToFields(fmtTpl) 118 | } 119 | 120 | // Template get 121 | func (f *TextFormatter) Template() string { 122 | return f.template 123 | } 124 | 125 | // WithEnableColor enable color on print log to terminal 126 | func (f *TextFormatter) WithEnableColor(enable bool) *TextFormatter { 127 | f.EnableColor = enable 128 | return f 129 | } 130 | 131 | // Fields get an export field list 132 | func (f *TextFormatter) Fields() []string { 133 | ss := make([]string, 0, len(f.fields)/2) 134 | for _, s := range f.fields { 135 | if s[0] >= 'a' && s[0] <= 'z' { 136 | ss = append(ss, s) 137 | } 138 | } 139 | return ss 140 | } 141 | 142 | var textPool bytebufferpool.Pool 143 | 144 | // Format a log record 145 | // 146 | //goland:noinspection GoUnhandledErrorResult 147 | func (f *TextFormatter) Format(r *Record) ([]byte, error) { 148 | f.beforeFormat() 149 | buf := textPool.Get() 150 | defer textPool.Put(buf) 151 | 152 | for _, field := range f.fields { 153 | // is not field name. eg: "}}] " 154 | if field[0] < 'a' || field[0] > 'z' { 155 | // remove left "}}" 156 | if len(field) > 1 && field[0:2] == "}}" { 157 | buf.WriteString(field[2:]) 158 | } else { 159 | buf.WriteString(field) 160 | } 161 | continue 162 | } 163 | 164 | switch { 165 | case field == FieldKeyDatetime: 166 | buf.B = r.Time.AppendFormat(buf.B, f.TimeFormat) 167 | case field == FieldKeyTimestamp: 168 | buf.WriteString(r.timestamp()) 169 | case field == FieldKeyCaller && r.Caller != nil: 170 | buf.WriteString(formatCaller(r.Caller, r.CallerFlag, f.CallerFormatFunc)) 171 | case field == FieldKeyLevel: 172 | buf.WriteString(f.renderColorText(field, r.LevelName(), r.Level)) 173 | case field == FieldKeyChannel: 174 | buf.WriteString(r.Channel) 175 | case field == FieldKeyMessage: 176 | buf.WriteString(f.renderColorText(field, r.Message, r.Level)) 177 | case field == FieldKeyData: 178 | if f.FullDisplay || len(r.Data) > 0 { 179 | buf.WriteString(f.EncodeFunc(r.Data)) 180 | } 181 | case field == FieldKeyExtra: 182 | if f.FullDisplay || len(r.Extra) > 0 { 183 | buf.WriteString(f.EncodeFunc(r.Extra)) 184 | } 185 | default: 186 | if _, ok := r.Fields[field]; ok { 187 | buf.WriteString(f.EncodeFunc(r.Fields[field])) 188 | } else { 189 | buf.WriteString(field) 190 | } 191 | } 192 | } 193 | 194 | // return buf.Bytes(), nil 195 | return buf.B, nil 196 | } 197 | 198 | func (f *TextFormatter) beforeFormat() { 199 | // if f.BeforeFunc == nil {} 200 | if f.EncodeFunc == nil { 201 | f.EncodeFunc = EncodeToString 202 | } 203 | if f.ColorTheme == nil { 204 | f.ColorTheme = ColorTheme 205 | } 206 | } 207 | 208 | func (f *TextFormatter) renderColorText(field, s string, l Level) string { 209 | // custom level name format 210 | if f.LevelFormatFunc != nil && field == FieldKeyLevel { 211 | s = f.LevelFormatFunc(s) 212 | } 213 | 214 | if !f.EnableColor { 215 | return s 216 | } 217 | 218 | // custom color render func 219 | if f.ColorRenderFunc != nil { 220 | return f.ColorRenderFunc(field, s, l) 221 | } 222 | 223 | // output colored logs for console output 224 | if theme, ok := f.ColorTheme[l]; ok { 225 | return theme.Render(s) 226 | } 227 | return s 228 | } 229 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gookit/slog 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/gookit/color v1.5.4 7 | github.com/gookit/goutil v0.6.18 8 | github.com/gookit/gsr v0.1.1 9 | github.com/valyala/bytebufferpool v1.0.0 10 | ) 11 | 12 | require ( 13 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 14 | golang.org/x/sync v0.11.0 // indirect 15 | golang.org/x/sys v0.30.0 // indirect 16 | golang.org/x/term v0.29.0 // indirect 17 | golang.org/x/text v0.22.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= 3 | github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= 4 | github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw= 5 | github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA= 6 | github.com/gookit/gsr v0.1.1 h1:TaHD3M7qa6lcAf9D2J4mGNg+QjgDtD1bw7uctF8RXOM= 7 | github.com/gookit/gsr v0.1.1/go.mod h1:7wv4Y4WCnil8+DlDYHBjidzrEzfHhXEoFjEA0pPPWpI= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 10 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 11 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 12 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 13 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 14 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 15 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 16 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 17 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 18 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 19 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 20 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 21 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 22 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import "io" 4 | 5 | // 6 | // Handler interface 7 | // 8 | 9 | // Handler interface definition 10 | type Handler interface { 11 | // Closer Close handler. 12 | // You should first call Flush() on close logic. 13 | // Refer the FileHandler.Close() handle 14 | io.Closer 15 | // Flush and sync logs to disk file. 16 | Flush() error 17 | // IsHandling Checks whether the given record will be handled by this handler. 18 | IsHandling(level Level) bool 19 | // Handle a log record. 20 | // 21 | // All records may be passed to this method, and the handler should discard 22 | // those that it does not want to handle. 23 | Handle(*Record) error 24 | } 25 | 26 | // LevelFormattable support limit log levels and provide formatter 27 | type LevelFormattable interface { 28 | Formattable 29 | IsHandling(level Level) bool 30 | } 31 | 32 | // FormattableHandler interface 33 | type FormattableHandler interface { 34 | Handler 35 | Formattable 36 | } 37 | 38 | /******************************************************************************** 39 | * Common parts for handler 40 | ********************************************************************************/ 41 | 42 | // LevelWithFormatter struct definition 43 | // 44 | // - support set log formatter 45 | // - only support set max log level 46 | type LevelWithFormatter struct { 47 | FormattableTrait 48 | // Level max for logging messages. if current level <= Level will log messages 49 | Level Level 50 | } 51 | 52 | // NewLvFormatter create new LevelWithFormatter instance 53 | func NewLvFormatter(maxLv Level) *LevelWithFormatter { 54 | return &LevelWithFormatter{Level: maxLv} 55 | } 56 | 57 | // SetMaxLevel set max level for logging messages 58 | func (h *LevelWithFormatter) SetMaxLevel(maxLv Level) { 59 | h.Level = maxLv 60 | } 61 | 62 | // IsHandling Check if the current level can be handling 63 | func (h *LevelWithFormatter) IsHandling(level Level) bool { 64 | return h.Level.ShouldHandling(level) 65 | } 66 | 67 | // LevelsWithFormatter struct definition 68 | // 69 | // - support set log formatter 70 | // - support setting multi log levels 71 | type LevelsWithFormatter struct { 72 | FormattableTrait 73 | // Levels for logging messages 74 | Levels []Level 75 | } 76 | 77 | // NewLvsFormatter create new instance 78 | func NewLvsFormatter(levels []Level) *LevelsWithFormatter { 79 | return &LevelsWithFormatter{Levels: levels} 80 | } 81 | 82 | // SetLimitLevels set limit levels for log message 83 | func (h *LevelsWithFormatter) SetLimitLevels(levels []Level) { 84 | h.Levels = levels 85 | } 86 | 87 | // IsHandling Check if the current level can be handling 88 | func (h *LevelsWithFormatter) IsHandling(level Level) bool { 89 | for _, l := range h.Levels { 90 | if l == level { 91 | return true 92 | } 93 | } 94 | return false 95 | } 96 | 97 | // LevelMode define level mode 98 | type LevelMode uint8 99 | 100 | // String return string value 101 | func (m LevelMode) String() string { 102 | switch m { 103 | case LevelModeList: 104 | return "list" 105 | case LevelModeMax: 106 | return "max" 107 | default: 108 | return "unknown" 109 | } 110 | } 111 | 112 | const ( 113 | // LevelModeList use level list for limit record write 114 | LevelModeList LevelMode = iota 115 | // LevelModeMax use max level limit log record write 116 | LevelModeMax 117 | ) 118 | 119 | // LevelHandling struct definition 120 | type LevelHandling struct { 121 | // level check mode. default is LevelModeList 122 | lvMode LevelMode 123 | // max level for log message. if current level <= Level will log message 124 | maxLevel Level 125 | // levels limit for log message 126 | levels []Level 127 | } 128 | 129 | // SetMaxLevel set max level for log message 130 | func (h *LevelHandling) SetMaxLevel(maxLv Level) { 131 | h.lvMode = LevelModeMax 132 | h.maxLevel = maxLv 133 | } 134 | 135 | // SetLimitLevels set limit levels for log message 136 | func (h *LevelHandling) SetLimitLevels(levels []Level) { 137 | h.lvMode = LevelModeList 138 | h.levels = levels 139 | } 140 | 141 | // IsHandling Check if the current level can be handling 142 | func (h *LevelHandling) IsHandling(level Level) bool { 143 | if h.lvMode == LevelModeMax { 144 | return h.maxLevel.ShouldHandling(level) 145 | } 146 | 147 | for _, l := range h.levels { 148 | if l == level { 149 | return true 150 | } 151 | } 152 | return false 153 | } 154 | 155 | // LevelFormatting wrap level handling and log formatter 156 | type LevelFormatting struct { 157 | LevelHandling 158 | FormatterWrapper 159 | } 160 | 161 | // NewMaxLevelFormatting create new instance with max level 162 | func NewMaxLevelFormatting(maxLevel Level) *LevelFormatting { 163 | lf := &LevelFormatting{} 164 | lf.SetMaxLevel(maxLevel) 165 | return lf 166 | } 167 | 168 | // NewLevelsFormatting create new instance with levels 169 | func NewLevelsFormatting(levels []Level) *LevelFormatting { 170 | lf := &LevelFormatting{} 171 | lf.SetLimitLevels(levels) 172 | return lf 173 | } 174 | -------------------------------------------------------------------------------- /handler/README.md: -------------------------------------------------------------------------------- 1 | # Handlers 2 | 3 | Package handler provide useful common log handlers. eg: file, console, multi_file, rotate_file, stream, syslog, email 4 | 5 | ```text 6 | handler -> buffered -> rotated -> writer(os.File) 7 | ``` 8 | 9 | ## Built-in handlers 10 | 11 | - `handler.ConsoleHandler` Console handler 12 | - `handler.FileHandler` File handler 13 | - `handler.StreamHandler` Stream handler 14 | - `handler.SyslogHandler` Syslog handler 15 | - `handler.EmailHandler` Email handler 16 | - `handler.FlushCloseHandler` Flush and close handler 17 | 18 | ## Go Docs 19 | 20 | Docs generated by: `go doc ./handler` 21 | 22 | ### Handler Functions 23 | 24 | ```go 25 | func LineBuffOsFile(f *os.File, bufSize int, levels []slog.Level) slog.Handler 26 | func LineBuffWriter(w io.Writer, bufSize int, levels []slog.Level) slog.Handler 27 | func LineBufferedFile(logfile string, bufSize int, levels []slog.Level) (slog.Handler, error) 28 | 29 | type ConsoleHandler = IOWriterHandler 30 | func ConsoleWithLevels(levels []slog.Level) *ConsoleHandler 31 | func ConsoleWithMaxLevel(level slog.Level) *ConsoleHandler 32 | func NewConsole(levels []slog.Level) *ConsoleHandler 33 | func NewConsoleHandler(levels []slog.Level) *ConsoleHandler 34 | func NewConsoleWithLF(lf slog.LevelFormattable) *ConsoleHandler 35 | type EmailHandler struct{ ... } 36 | func NewEmailHandler(from EmailOption, toAddresses []string) *EmailHandler 37 | type EmailOption struct{ ... } 38 | 39 | type FlushCloseHandler struct{ ... } 40 | func FlushCloserWithLevels(out FlushCloseWriter, levels []slog.Level) *FlushCloseHandler 41 | func FlushCloserWithMaxLevel(out FlushCloseWriter, maxLevel slog.Level) *FlushCloseHandler 42 | func NewBuffered(w io.WriteCloser, bufSize int, levels ...slog.Level) *FlushCloseHandler 43 | func NewBufferedHandler(w io.WriteCloser, bufSize int, levels ...slog.Level) *FlushCloseHandler 44 | func NewFlushCloseHandler(out FlushCloseWriter, levels []slog.Level) *FlushCloseHandler 45 | func NewFlushCloser(out FlushCloseWriter, levels []slog.Level) *FlushCloseHandler 46 | func NewFlushCloserWithLF(out FlushCloseWriter, lf slog.LevelFormattable) *FlushCloseHandler 47 | 48 | type IOWriterHandler struct{ ... } 49 | func IOWriterWithLevels(out io.Writer, levels []slog.Level) *IOWriterHandler 50 | func IOWriterWithMaxLevel(out io.Writer, maxLevel slog.Level) *IOWriterHandler 51 | func NewIOWriter(out io.Writer, levels []slog.Level) *IOWriterHandler 52 | func NewIOWriterHandler(out io.Writer, levels []slog.Level) *IOWriterHandler 53 | func NewIOWriterWithLF(out io.Writer, lf slog.LevelFormattable) *IOWriterHandler 54 | func NewSimpleHandler(out io.Writer, maxLevel slog.Level) *IOWriterHandler 55 | func SimpleWithLevels(out io.Writer, levels []slog.Level) *IOWriterHandler 56 | 57 | 58 | type SimpleHandler = IOWriterHandler 59 | func NewHandler(out io.Writer, maxLevel slog.Level) *SimpleHandler 60 | func NewSimple(out io.Writer, maxLevel slog.Level) *SimpleHandler 61 | 62 | type SyncCloseHandler struct{ ... } 63 | func JSONFileHandler(logfile string, fns ...ConfigFn) (*SyncCloseHandler, error) 64 | func MustFileHandler(logfile string, fns ...ConfigFn) *SyncCloseHandler 65 | func MustRotateFile(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) *SyncCloseHandler 66 | func MustSimpleFile(filepath string, maxLv ...slog.Level) *SyncCloseHandler 67 | func MustSizeRotateFile(logfile string, maxSize int, fns ...ConfigFn) *SyncCloseHandler 68 | func MustTimeRotateFile(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) *SyncCloseHandler 69 | func NewBuffFileHandler(logfile string, buffSize int, fns ...ConfigFn) (*SyncCloseHandler, error) 70 | func NewFileHandler(logfile string, fns ...ConfigFn) (h *SyncCloseHandler, err error) 71 | func NewRotateFile(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) (*SyncCloseHandler, error) 72 | func NewRotateFileHandler(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) (*SyncCloseHandler, error) 73 | func NewSimpleFile(filepath string, maxLv ...slog.Level) (*SyncCloseHandler, error) 74 | func NewSimpleFileHandler(filePath string, maxLv ...slog.Level) (*SyncCloseHandler, error) 75 | func NewSizeRotateFile(logfile string, maxSize int, fns ...ConfigFn) (*SyncCloseHandler, error) 76 | func NewSizeRotateFileHandler(logfile string, maxSize int, fns ...ConfigFn) (*SyncCloseHandler, error) 77 | func NewSyncCloseHandler(out SyncCloseWriter, levels []slog.Level) *SyncCloseHandler 78 | func NewSyncCloser(out SyncCloseWriter, levels []slog.Level) *SyncCloseHandler 79 | func NewSyncCloserWithLF(out SyncCloseWriter, lf slog.LevelFormattable) *SyncCloseHandler 80 | func NewTimeRotateFile(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) (*SyncCloseHandler, error) 81 | func NewTimeRotateFileHandler(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) (*SyncCloseHandler, error) 82 | func SyncCloserWithLevels(out SyncCloseWriter, levels []slog.Level) *SyncCloseHandler 83 | func SyncCloserWithMaxLevel(out SyncCloseWriter, maxLevel slog.Level) *SyncCloseHandler 84 | 85 | type SysLogHandler struct{ ... } 86 | func NewSysLogHandler(priority syslog.Priority, tag string) (*SysLogHandler, error) 87 | 88 | type WriteCloserHandler struct{ ... } 89 | func NewWriteCloser(out io.WriteCloser, levels []slog.Level) *WriteCloserHandler 90 | func NewWriteCloserHandler(out io.WriteCloser, levels []slog.Level) *WriteCloserHandler 91 | func NewWriteCloserWithLF(out io.WriteCloser, lf slog.LevelFormattable) *WriteCloserHandler 92 | func WriteCloserWithLevels(out io.WriteCloser, levels []slog.Level) *WriteCloserHandler 93 | func WriteCloserWithMaxLevel(out io.WriteCloser, maxLevel slog.Level) *WriteCloserHandler 94 | ``` 95 | 96 | 97 | ### Config Functions 98 | 99 | ```go 100 | type Builder struct{ ... } 101 | func NewBuilder() *Builder 102 | type Config struct{ ... } 103 | func NewConfig(fns ...ConfigFn) *Config 104 | func NewEmptyConfig(fns ...ConfigFn) *Config 105 | type ConfigFn func(c *Config) 106 | func WithBackupNum(n uint) ConfigFn 107 | func WithBackupTime(bt uint) ConfigFn 108 | func WithBuffMode(buffMode string) ConfigFn 109 | func WithBuffSize(buffSize int) ConfigFn 110 | func WithCompress(compress bool) ConfigFn 111 | func WithFilePerm(filePerm fs.FileMode) ConfigFn 112 | func WithLevelMode(mode slog.LevelMode) ConfigFn 113 | func WithLevelName(name string) ConfigFn 114 | func WithLevelNames(names []string) ConfigFn 115 | func WithLevelNamesString(names string) ConfigFn 116 | func WithLogLevel(level slog.Level) ConfigFn 117 | func WithLogLevels(levels slog.Levels) ConfigFn 118 | func WithLogfile(logfile string) ConfigFn 119 | func WithMaxLevelName(name string) ConfigFn 120 | func WithMaxSize(maxSize uint64) ConfigFn 121 | func WithRotateMode(m rotatefile.RotateMode) ConfigFn 122 | func WithRotateTime(rt rotatefile.RotateTime) ConfigFn 123 | func WithRotateTimeString(rt string) ConfigFn 124 | func WithTimeClock(clock rotatefile.Clocker) ConfigFn 125 | func WithUseJSON(useJSON bool) ConfigFn 126 | ``` 127 | -------------------------------------------------------------------------------- /handler/buffer.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/gookit/slog" 8 | "github.com/gookit/slog/bufwrite" 9 | ) 10 | 11 | // NewBuffered create new BufferedHandler 12 | func NewBuffered(w io.WriteCloser, bufSize int, levels ...slog.Level) *FlushCloseHandler { 13 | return NewBufferedHandler(w, bufSize, levels...) 14 | } 15 | 16 | // NewBufferedHandler create new BufferedHandler 17 | func NewBufferedHandler(w io.WriteCloser, bufSize int, levels ...slog.Level) *FlushCloseHandler { 18 | if len(levels) == 0 { 19 | levels = slog.AllLevels 20 | } 21 | 22 | out := bufwrite.NewBufIOWriterSize(w, bufSize) 23 | return FlushCloserWithLevels(out, levels) 24 | } 25 | 26 | // LineBufferedFile handler 27 | func LineBufferedFile(logfile string, bufSize int, levels []slog.Level) (slog.Handler, error) { 28 | cfg := NewConfig( 29 | WithLogfile(logfile), 30 | WithBuffSize(bufSize), 31 | WithLogLevels(levels), 32 | WithBuffMode(BuffModeLine), 33 | ) 34 | 35 | out, err := cfg.CreateWriter() 36 | if err != nil { 37 | return nil, err 38 | } 39 | return SyncCloserWithLevels(out, levels), nil 40 | } 41 | 42 | // LineBuffOsFile handler 43 | func LineBuffOsFile(f *os.File, bufSize int, levels []slog.Level) slog.Handler { 44 | if f == nil { 45 | panic("slog: the os file cannot be nil") 46 | } 47 | 48 | out := bufwrite.NewLineWriterSize(f, bufSize) 49 | return SyncCloserWithLevels(out, levels) 50 | } 51 | 52 | // LineBuffWriter handler 53 | func LineBuffWriter(w io.Writer, bufSize int, levels []slog.Level) slog.Handler { 54 | if w == nil { 55 | panic("slog: the io writer cannot be nil") 56 | } 57 | 58 | out := bufwrite.NewLineWriterSize(w, bufSize) 59 | return IOWriterWithLevels(out, levels) 60 | } 61 | 62 | // 63 | // --------- wrap a handler with buffer --------- 64 | // 65 | 66 | // FormatWriterHandler interface 67 | type FormatWriterHandler interface { 68 | slog.Handler 69 | // Formatter record formatter 70 | Formatter() slog.Formatter 71 | // Writer the output writer 72 | Writer() io.Writer 73 | } 74 | -------------------------------------------------------------------------------- /handler/buffer_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/gookit/goutil/fsutil" 8 | "github.com/gookit/goutil/testutil/assert" 9 | "github.com/gookit/slog" 10 | "github.com/gookit/slog/handler" 11 | ) 12 | 13 | func TestNewBufferedHandler(t *testing.T) { 14 | logfile := "./testdata/buffer-os-file.log" 15 | assert.NoErr(t, fsutil.DeleteIfFileExist(logfile)) 16 | 17 | file, err := handler.QuickOpenFile(logfile) 18 | assert.NoErr(t, err) 19 | assert.True(t, fsutil.IsFile(logfile)) 20 | 21 | bh := handler.NewBuffered(file, 128) 22 | 23 | // new logger 24 | l := slog.NewWithHandlers(bh) 25 | l.Info("buffered info message") 26 | 27 | bts, err := os.ReadFile(logfile) 28 | assert.NoErr(t, err) 29 | assert.Empty(t, bts) 30 | 31 | l.Warn("buffered warn message") 32 | bts, err = os.ReadFile(logfile) 33 | assert.NoErr(t, err) 34 | 35 | str := string(bts) 36 | assert.Contains(t, str, "[INFO]") 37 | 38 | err = l.FlushAll() 39 | assert.NoErr(t, err) 40 | } 41 | 42 | func TestLineBufferedFile(t *testing.T) { 43 | logfile := "./testdata/line-buff-file.log" 44 | assert.NoErr(t, fsutil.DeleteIfFileExist(logfile)) 45 | 46 | h, err := handler.LineBufferedFile(logfile, 12, slog.AllLevels) 47 | assert.NoErr(t, err) 48 | assert.True(t, fsutil.IsFile(logfile)) 49 | 50 | r := newLogRecord("Test LineBufferedFile") 51 | err = h.Handle(r) 52 | assert.NoErr(t, err) 53 | 54 | bts, err := os.ReadFile(logfile) 55 | assert.NoErr(t, err) 56 | 57 | str := string(bts) 58 | assert.Contains(t, str, "[INFO]") 59 | assert.Contains(t, str, "Test LineBufferedFile") 60 | } 61 | 62 | func TestLineBuffOsFile(t *testing.T) { 63 | logfile := "./testdata/line-buff-os-file.log" 64 | assert.NoErr(t, fsutil.DeleteIfFileExist(logfile)) 65 | 66 | file, err := fsutil.QuickOpenFile(logfile) 67 | assert.NoErr(t, err) 68 | 69 | h := handler.LineBuffOsFile(file, 12, slog.AllLevels) 70 | assert.NoErr(t, err) 71 | assert.True(t, fsutil.IsFile(logfile)) 72 | 73 | r := newLogRecord("Test LineBuffOsFile") 74 | err = h.Handle(r) 75 | assert.NoErr(t, err) 76 | 77 | bts, err := os.ReadFile(logfile) 78 | assert.NoErr(t, err) 79 | 80 | str := string(bts) 81 | assert.Contains(t, str, "[INFO]") 82 | assert.Contains(t, str, "Test LineBuffOsFile") 83 | 84 | assert.Panics(t, func() { 85 | handler.LineBuffOsFile(nil, 12, slog.AllLevels) 86 | }) 87 | } 88 | 89 | func TestLineBuffWriter(t *testing.T) { 90 | logfile := "./testdata/line-buff-writer.log" 91 | assert.NoErr(t, fsutil.DeleteIfFileExist(logfile)) 92 | 93 | file, err := fsutil.QuickOpenFile(logfile) 94 | assert.NoErr(t, err) 95 | 96 | h := handler.LineBuffWriter(file, 12, slog.AllLevels) 97 | assert.NoErr(t, err) 98 | assert.True(t, fsutil.IsFile(logfile)) 99 | assert.Panics(t, func() { 100 | handler.LineBuffWriter(nil, 12, slog.AllLevels) 101 | }) 102 | 103 | r := newLogRecord("Test LineBuffWriter") 104 | err = h.Handle(r) 105 | assert.NoErr(t, err) 106 | 107 | bts, err := os.ReadFile(logfile) 108 | assert.NoErr(t, err) 109 | 110 | str := string(bts) 111 | assert.Contains(t, str, "[INFO]") 112 | assert.Contains(t, str, "Test LineBuffWriter") 113 | 114 | assert.Panics(t, func() { 115 | handler.LineBuffOsFile(nil, 12, slog.AllLevels) 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /handler/builder.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/gookit/slog" 7 | "github.com/gookit/slog/rotatefile" 8 | ) 9 | 10 | // 11 | // --------------------------------------------------------------------------- 12 | // handler builder 13 | // --------------------------------------------------------------------------- 14 | // 15 | 16 | // Builder struct for create handler 17 | type Builder struct { 18 | *Config 19 | Output io.Writer 20 | } 21 | 22 | // NewBuilder create 23 | func NewBuilder() *Builder { 24 | return &Builder{ 25 | Config: NewEmptyConfig(), 26 | } 27 | } 28 | 29 | // WithOutput to the builder 30 | func (b *Builder) WithOutput(w io.Writer) *Builder { 31 | b.Output = w 32 | return b 33 | } 34 | 35 | // With some config fn 36 | // 37 | // Deprecated: please use WithConfigFn() 38 | func (b *Builder) With(fns ...ConfigFn) *Builder { 39 | return b.WithConfigFn(fns...) 40 | } 41 | 42 | // WithConfigFn some config fn 43 | func (b *Builder) WithConfigFn(fns ...ConfigFn) *Builder { 44 | b.Config.With(fns...) 45 | return b 46 | } 47 | 48 | // WithLogfile setting 49 | func (b *Builder) WithLogfile(logfile string) *Builder { 50 | b.Logfile = logfile 51 | return b 52 | } 53 | 54 | // WithLevelMode setting 55 | func (b *Builder) WithLevelMode(mode slog.LevelMode) *Builder { 56 | b.LevelMode = mode 57 | return b 58 | } 59 | 60 | // WithLogLevel setting max log level 61 | func (b *Builder) WithLogLevel(level slog.Level) *Builder { 62 | b.Level = level 63 | b.LevelMode = slog.LevelModeMax 64 | return b 65 | } 66 | 67 | // WithLogLevels setting 68 | func (b *Builder) WithLogLevels(levels []slog.Level) *Builder { 69 | b.Levels = levels 70 | b.LevelMode = slog.LevelModeList 71 | return b 72 | } 73 | 74 | // WithBuffMode setting 75 | func (b *Builder) WithBuffMode(bufMode string) *Builder { 76 | b.BuffMode = bufMode 77 | return b 78 | } 79 | 80 | // WithBuffSize setting 81 | func (b *Builder) WithBuffSize(bufSize int) *Builder { 82 | b.BuffSize = bufSize 83 | return b 84 | } 85 | 86 | // WithMaxSize setting 87 | func (b *Builder) WithMaxSize(maxSize uint64) *Builder { 88 | b.MaxSize = maxSize 89 | return b 90 | } 91 | 92 | // WithRotateTime setting 93 | func (b *Builder) WithRotateTime(rt rotatefile.RotateTime) *Builder { 94 | b.RotateTime = rt 95 | return b 96 | } 97 | 98 | // WithCompress setting 99 | func (b *Builder) WithCompress(compress bool) *Builder { 100 | b.Compress = compress 101 | return b 102 | } 103 | 104 | // WithUseJSON setting 105 | func (b *Builder) WithUseJSON(useJSON bool) *Builder { 106 | b.UseJSON = useJSON 107 | return b 108 | } 109 | 110 | // Build slog handler. 111 | func (b *Builder) Build() slog.FormattableHandler { 112 | if b.Output != nil { 113 | return b.buildFromWriter(b.Output) 114 | } 115 | 116 | if b.Logfile != "" { 117 | w, err := b.CreateWriter() 118 | if err != nil { 119 | panic(err) 120 | } 121 | return b.buildFromWriter(w) 122 | } 123 | 124 | panic("slog: missing information for build slog handler") 125 | } 126 | 127 | // Build slog handler. 128 | func (b *Builder) buildFromWriter(w io.Writer) (h slog.FormattableHandler) { 129 | defer b.reset() 130 | bufSize := b.BuffSize 131 | lf := b.newLevelFormattable() 132 | 133 | if scw, ok := w.(SyncCloseWriter); ok { 134 | if bufSize > 0 { 135 | scw = b.wrapBuffer(scw) 136 | } 137 | 138 | h = NewSyncCloserWithLF(scw, lf) 139 | } else if fcw, ok := w.(FlushCloseWriter); ok { 140 | if bufSize > 0 { 141 | fcw = b.wrapBuffer(fcw) 142 | } 143 | 144 | h = NewFlushCloserWithLF(fcw, lf) 145 | } else if wc, ok := w.(io.WriteCloser); ok { 146 | if bufSize > 0 { 147 | wc = b.wrapBuffer(wc) 148 | } 149 | 150 | h = NewWriteCloserWithLF(wc, lf) 151 | } else { 152 | if bufSize > 0 { 153 | w = b.wrapBuffer(w) 154 | } 155 | 156 | h = NewIOWriterWithLF(w, lf) 157 | } 158 | 159 | // use json format. 160 | if b.UseJSON { 161 | h.SetFormatter(slog.NewJSONFormatter()) 162 | } 163 | return 164 | } 165 | 166 | // rest builder. 167 | func (b *Builder) reset() { 168 | b.Output = nil 169 | b.Config = NewEmptyConfig() 170 | } 171 | -------------------------------------------------------------------------------- /handler/config_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/gookit/goutil/errorx" 8 | "github.com/gookit/goutil/fmtutil" 9 | "github.com/gookit/goutil/fsutil" 10 | "github.com/gookit/goutil/testutil/assert" 11 | "github.com/gookit/slog" 12 | "github.com/gookit/slog/handler" 13 | "github.com/gookit/slog/rotatefile" 14 | ) 15 | 16 | func TestNewConfig(t *testing.T) { 17 | c := handler.NewConfig( 18 | handler.WithCompress(true), 19 | handler.WithLevelMode(handler.LevelModeValue), 20 | handler.WithBackupNum(20), 21 | handler.WithBackupTime(1800), 22 | handler.WithRotateMode(rotatefile.ModeCreate), 23 | func(c *handler.Config) { 24 | c.BackupTime = 23 25 | c.RenameFunc = func(fpath string, num uint) string { 26 | return fpath + ".bak" 27 | } 28 | }, 29 | ). 30 | With(handler.WithBuffSize(129)). 31 | WithConfigFn(handler.WithLogLevel(slog.ErrorLevel)) 32 | 33 | assert.True(t, c.Compress) 34 | assert.Eq(t, 129, c.BuffSize) 35 | assert.Eq(t, handler.LevelModeValue, c.LevelMode) 36 | assert.Eq(t, slog.ErrorLevel, c.Level) 37 | assert.Eq(t, rotatefile.ModeCreate, c.RotateMode) 38 | 39 | c.WithConfigFn(handler.WithLevelNames([]string{"info", "debug"})) 40 | assert.Eq(t, []slog.Level{slog.InfoLevel, slog.DebugLevel}, c.Levels) 41 | } 42 | 43 | func TestWithLevelNamesString(t *testing.T) { 44 | c := handler.NewConfig(handler.WithLevelNamesString("info,error")) 45 | assert.Eq(t, []slog.Level{slog.InfoLevel, slog.ErrorLevel}, c.Levels) 46 | } 47 | 48 | func TestWithMaxLevelName(t *testing.T) { 49 | c := handler.NewConfig(handler.WithMaxLevelName("error")) 50 | assert.Eq(t, slog.ErrorLevel, c.Level) 51 | assert.Eq(t, handler.LevelModeValue, c.LevelMode) 52 | 53 | c1 := handler.NewConfig(handler.WithLevelName("warn")) 54 | assert.Eq(t, slog.WarnLevel, c1.Level) 55 | assert.Eq(t, handler.LevelModeValue, c1.LevelMode) 56 | } 57 | 58 | func TestWithRotateTimeString(t *testing.T) { 59 | tests := []struct { 60 | input string 61 | expected rotatefile.RotateTime 62 | panics bool 63 | }{ 64 | {"1hours", rotatefile.RotateTime(3600), false}, 65 | {"24h", rotatefile.RotateTime(86400), false}, 66 | {"1day", rotatefile.RotateTime(86400), false}, 67 | {"7d", rotatefile.RotateTime(604800), false}, 68 | {"1m", rotatefile.RotateTime(60), false}, 69 | {"30s", rotatefile.RotateTime(30), false}, 70 | {"invalid", 0, true}, 71 | } 72 | 73 | for _, tt := range tests { 74 | t.Run(tt.input, func(t *testing.T) { 75 | c := &handler.Config{} 76 | if tt.panics { 77 | assert.Panics(t, func() { 78 | handler.WithRotateTimeString(tt.input)(c) 79 | }) 80 | } else { 81 | assert.NotPanics(t, func() { 82 | handler.WithRotateTimeString(tt.input)(c) 83 | }) 84 | assert.Eq(t, tt.expected, c.RotateTime) 85 | } 86 | }) 87 | } 88 | } 89 | 90 | func TestNewBuilder(t *testing.T) { 91 | testFile := "testdata/builder.log" 92 | assert.NoErr(t, fsutil.DeleteIfFileExist(testFile)) 93 | 94 | b := handler.NewBuilder(). 95 | WithLogfile(testFile). 96 | WithLogLevels(slog.AllLevels). 97 | WithBuffSize(128). 98 | WithBuffMode(handler.BuffModeBite). 99 | WithMaxSize(fmtutil.OneMByte * 3). 100 | WithRotateTime(rotatefile.Every30Min). 101 | WithCompress(true). 102 | With(func(c *handler.Config) { 103 | c.BackupNum = 3 104 | }) 105 | 106 | assert.Eq(t, uint(3), b.BackupNum) 107 | assert.Eq(t, handler.BuffModeBite, b.BuffMode) 108 | assert.Eq(t, rotatefile.Every30Min, b.RotateTime) 109 | 110 | h := b.Build() 111 | assert.NotNil(t, h) 112 | assert.NoErr(t, h.Close()) 113 | 114 | b1 := handler.NewBuilder(). 115 | WithOutput(new(bytes.Buffer)). 116 | WithUseJSON(true). 117 | WithLogLevel(slog.ErrorLevel). 118 | WithLevelMode(handler.LevelModeValue) 119 | assert.Eq(t, handler.LevelModeValue, b1.LevelMode) 120 | assert.Eq(t, slog.ErrorLevel, b1.Level) 121 | 122 | h2 := b1.Build() 123 | assert.NotNil(t, h2) 124 | 125 | assert.Panics(t, func() { 126 | handler.NewBuilder().Build() 127 | }) 128 | } 129 | 130 | type simpleWriter struct { 131 | errOnWrite bool 132 | } 133 | 134 | func (w *simpleWriter) Write(p []byte) (n int, err error) { 135 | if w.errOnWrite { 136 | return 0, errorx.Raw("write error") 137 | } 138 | return len(p), nil 139 | } 140 | 141 | type closeWriter struct { 142 | errOnWrite bool 143 | errOnClose bool 144 | } 145 | 146 | func (w *closeWriter) Close() error { 147 | if w.errOnClose { 148 | return errorx.Raw("close error") 149 | } 150 | return nil 151 | } 152 | 153 | func (w *closeWriter) Write(p []byte) (n int, err error) { 154 | if w.errOnWrite { 155 | return 0, errorx.Raw("write error") 156 | } 157 | return len(p), nil 158 | } 159 | 160 | type flushCloseWriter struct { 161 | closeWriter 162 | errOnFlush bool 163 | } 164 | 165 | // Flush implement stdio.Flusher 166 | func (w *flushCloseWriter) Flush() error { 167 | if w.errOnFlush { 168 | return errorx.Raw("flush error") 169 | } 170 | return nil 171 | } 172 | 173 | type syncCloseWriter struct { 174 | closeWriter 175 | errOnSync bool 176 | } 177 | 178 | // Sync implement stdio.Syncer 179 | func (w *syncCloseWriter) Sync() error { 180 | if w.errOnSync { 181 | return errorx.Raw("sync error") 182 | } 183 | return nil 184 | } 185 | 186 | func TestNewBuilder_buildFromWriter(t *testing.T) { 187 | t.Run("FlushCloseWriter", func(t *testing.T) { 188 | out := &flushCloseWriter{} 189 | out.errOnFlush = true 190 | h := handler.NewBuilder(). 191 | WithOutput(out). 192 | WithConfigFn(func(c *handler.Config) { 193 | c.RenameFunc = func(fpath string, num uint) string { 194 | return fpath + ".bak" 195 | } 196 | }). 197 | Build() 198 | assert.Err(t, h.Flush()) 199 | 200 | // wrap buffer 201 | h = handler.NewBuilder(). 202 | WithOutput(out). 203 | WithBuffSize(128). 204 | Build() 205 | assert.NoErr(t, h.Close()) 206 | assert.NoErr(t, h.Flush()) 207 | }) 208 | 209 | t.Run("CloseWriter", func(t *testing.T) { 210 | h := handler.NewBuilder(). 211 | WithOutput(&closeWriter{errOnClose: true}). 212 | WithBuffSize(128). 213 | Build() 214 | assert.NotNil(t, h) 215 | assert.Err(t, h.Close()) 216 | }) 217 | 218 | t.Run("SimpleWriter", func(t *testing.T) { 219 | h := handler.NewBuilder(). 220 | WithOutput(&simpleWriter{errOnWrite: true}). 221 | WithBuffSize(128). 222 | Build() 223 | assert.NotNil(t, h) 224 | assert.NoErr(t, h.Close()) 225 | }) 226 | } 227 | -------------------------------------------------------------------------------- /handler/console.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gookit/color" 7 | "github.com/gookit/slog" 8 | ) 9 | 10 | /******************************************************************************** 11 | * console log handler 12 | ********************************************************************************/ 13 | 14 | // ConsoleHandler definition 15 | type ConsoleHandler = IOWriterHandler 16 | 17 | // NewConsoleWithLF create new ConsoleHandler and with custom slog.LevelFormattable 18 | func NewConsoleWithLF(lf slog.LevelFormattable) *ConsoleHandler { 19 | h := NewIOWriterWithLF(os.Stdout, lf) 20 | 21 | // default use text formatter 22 | f := slog.NewTextFormatter() 23 | // default enable color on console 24 | f.WithEnableColor(color.SupportColor()) 25 | 26 | h.SetFormatter(f) 27 | return h 28 | } 29 | 30 | // 31 | // ------------- Use max log level ------------- 32 | // 33 | 34 | // ConsoleWithMaxLevel create new ConsoleHandler and with max log level 35 | func ConsoleWithMaxLevel(level slog.Level) *ConsoleHandler { 36 | return NewConsoleWithLF(slog.NewLvFormatter(level)) 37 | } 38 | 39 | // 40 | // ------------- Use multi log levels ------------- 41 | // 42 | 43 | // NewConsole create new ConsoleHandler, alias of NewConsoleHandler 44 | func NewConsole(levels []slog.Level) *ConsoleHandler { 45 | return NewConsoleHandler(levels) 46 | } 47 | 48 | // ConsoleWithLevels create new ConsoleHandler and with limited log levels 49 | func ConsoleWithLevels(levels []slog.Level) *ConsoleHandler { 50 | return NewConsoleHandler(levels) 51 | } 52 | 53 | // NewConsoleHandler create new ConsoleHandler with limited log levels 54 | func NewConsoleHandler(levels []slog.Level) *ConsoleHandler { 55 | return NewConsoleWithLF(slog.NewLvsFormatter(levels)) 56 | } 57 | -------------------------------------------------------------------------------- /handler/console_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/goutil/testutil/assert" 7 | "github.com/gookit/slog" 8 | "github.com/gookit/slog/handler" 9 | ) 10 | 11 | func TestConsoleWithMaxLevel(t *testing.T) { 12 | l := slog.NewWithHandlers(handler.ConsoleWithMaxLevel(slog.InfoLevel)) 13 | l.DoNothingOnPanicFatal() 14 | 15 | for _, level := range slog.AllLevels { 16 | l.Log(level, "a test message") 17 | } 18 | assert.NoErr(t, l.LastErr()) 19 | } 20 | -------------------------------------------------------------------------------- /handler/email.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/smtp" 5 | "strconv" 6 | 7 | "github.com/gookit/slog" 8 | ) 9 | 10 | // EmailOption struct 11 | type EmailOption struct { 12 | SMTPHost string `json:"smtp_host"` // eg "smtp.gmail.com" 13 | SMTPPort int `json:"smtp_port"` // eg 587 14 | FromAddr string `json:"from_addr"` // eg "yourEmail@gmail.com" 15 | Password string `json:"password"` 16 | } 17 | 18 | // EmailHandler struct 19 | type EmailHandler struct { 20 | NopFlushClose 21 | slog.LevelWithFormatter 22 | // From the sender email information 23 | From EmailOption 24 | // ToAddresses email list 25 | ToAddresses []string 26 | } 27 | 28 | // NewEmailHandler instance 29 | func NewEmailHandler(from EmailOption, toAddresses []string) *EmailHandler { 30 | h := &EmailHandler{ 31 | From: from, 32 | // to receivers 33 | ToAddresses: toAddresses, 34 | } 35 | 36 | // init default log level 37 | h.Level = slog.InfoLevel 38 | return h 39 | } 40 | 41 | // Handle a log record 42 | func (h *EmailHandler) Handle(r *slog.Record) error { 43 | msgBytes, err := h.Format(r) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | var auth = smtp.PlainAuth("", h.From.FromAddr, h.From.Password, h.From.SMTPHost) 49 | addr := h.From.SMTPHost + ":" + strconv.Itoa(h.From.SMTPPort) 50 | 51 | return smtp.SendMail(addr, auth, h.From.FromAddr, h.ToAddresses, msgBytes) 52 | } 53 | -------------------------------------------------------------------------------- /handler/example_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "github.com/gookit/slog" 5 | "github.com/gookit/slog/handler" 6 | ) 7 | 8 | func Example_fileHandler() { 9 | withLevels := handler.WithLogLevels(slog.Levels{slog.PanicLevel, slog.ErrorLevel, slog.WarnLevel}) 10 | h1 := handler.MustFileHandler("/tmp/error.log", withLevels) 11 | 12 | withLevels = handler.WithLogLevels(slog.Levels{slog.InfoLevel, slog.NoticeLevel, slog.DebugLevel, slog.TraceLevel}) 13 | h2 := handler.MustFileHandler("/tmp/info.log", withLevels) 14 | 15 | slog.PushHandler(h1) 16 | slog.PushHandler(h2) 17 | 18 | // add logs 19 | slog.Info("info message") 20 | slog.Error("error message") 21 | } 22 | 23 | func Example_rotateFileHandler() { 24 | h1 := handler.MustRotateFile("/tmp/error.log", handler.EveryHour, handler.WithLogLevels(slog.DangerLevels)) 25 | h2 := handler.MustRotateFile("/tmp/info.log", handler.EveryHour, handler.WithLogLevels(slog.NormalLevels)) 26 | 27 | slog.PushHandler(h1) 28 | slog.PushHandler(h2) 29 | 30 | // add logs 31 | slog.Info("info message") 32 | slog.Error("error message") 33 | } 34 | -------------------------------------------------------------------------------- /handler/file.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gookit/goutil/basefn" 5 | "github.com/gookit/slog" 6 | ) 7 | 8 | // JSONFileHandler create new FileHandler with JSON formatter 9 | func JSONFileHandler(logfile string, fns ...ConfigFn) (*SyncCloseHandler, error) { 10 | return NewFileHandler(logfile, append(fns, WithUseJSON(true))...) 11 | } 12 | 13 | // NewBuffFileHandler create file handler with buff size 14 | func NewBuffFileHandler(logfile string, buffSize int, fns ...ConfigFn) (*SyncCloseHandler, error) { 15 | return NewFileHandler(logfile, append(fns, WithBuffSize(buffSize))...) 16 | } 17 | 18 | // MustFileHandler create file handler 19 | func MustFileHandler(logfile string, fns ...ConfigFn) *SyncCloseHandler { 20 | return basefn.Must(NewFileHandler(logfile, fns...)) 21 | } 22 | 23 | // NewFileHandler create new FileHandler 24 | func NewFileHandler(logfile string, fns ...ConfigFn) (h *SyncCloseHandler, err error) { 25 | return NewEmptyConfig(fns...).With(WithLogfile(logfile)).CreateHandler() 26 | } 27 | 28 | // 29 | // ------------- simple file handler ------------- 30 | // 31 | 32 | // MustSimpleFile new instance 33 | func MustSimpleFile(filepath string, maxLv ...slog.Level) *SyncCloseHandler { 34 | return basefn.Must(NewSimpleFileHandler(filepath, maxLv...)) 35 | } 36 | 37 | // NewSimpleFile new instance 38 | func NewSimpleFile(filepath string, maxLv ...slog.Level) (*SyncCloseHandler, error) { 39 | return NewSimpleFileHandler(filepath, maxLv...) 40 | } 41 | 42 | // NewSimpleFileHandler instance, default log level is InfoLevel 43 | // 44 | // Usage: 45 | // 46 | // h, err := NewSimpleFileHandler("/tmp/error.log") 47 | // 48 | // Custom formatter: 49 | // 50 | // h.SetFormatter(slog.NewJSONFormatter()) 51 | // slog.PushHandler(h) 52 | // slog.Info("log message") 53 | func NewSimpleFileHandler(filePath string, maxLv ...slog.Level) (*SyncCloseHandler, error) { 54 | file, err := QuickOpenFile(filePath) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | h := SyncCloserWithMaxLevel(file, basefn.FirstOr(maxLv, slog.InfoLevel)) 60 | return h, nil 61 | } 62 | -------------------------------------------------------------------------------- /handler/file_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/gookit/goutil/fsutil" 8 | "github.com/gookit/goutil/testutil/assert" 9 | "github.com/gookit/slog" 10 | "github.com/gookit/slog/handler" 11 | ) 12 | 13 | // const testSubFile = "./testdata/subdir/app.log" 14 | 15 | func TestNewFileHandler(t *testing.T) { 16 | testFile := "testdata/file.log" 17 | h, err := handler.NewFileHandler(testFile, handler.WithFilePerm(0644)) 18 | assert.NoErr(t, err) 19 | 20 | l := slog.NewWithHandlers(h) 21 | l.DoNothingOnPanicFatal() 22 | l.Info("info message") 23 | l.Warn("warn message") 24 | logAllLevel(l, "file handler message") 25 | 26 | assert.True(t, fsutil.IsFile(testFile)) 27 | 28 | str, err := fsutil.ReadStringOrErr(testFile) 29 | assert.NoErr(t, err) 30 | 31 | assert.Contains(t, str, "[INFO]") 32 | assert.Contains(t, str, "info message") 33 | assert.Contains(t, str, "[WARNING]") 34 | assert.Contains(t, str, "warn message") 35 | 36 | // assert.NoErr(t, os.Remove(testFile)) 37 | } 38 | 39 | func TestMustFileHandler(t *testing.T) { 40 | testFile := "testdata/file-must.log" 41 | 42 | h := handler.MustFileHandler(testFile) 43 | assert.NotEmpty(t, h.Writer()) 44 | 45 | r := newLogRecord("test file must handler") 46 | 47 | err := h.Handle(r) 48 | assert.NoErr(t, err) 49 | assert.NoErr(t, h.Close()) 50 | 51 | bts := fsutil.MustReadFile(testFile) 52 | str := string(bts) 53 | 54 | assert.Contains(t, str, `INFO`) 55 | assert.Contains(t, str, `test file must handler`) 56 | } 57 | 58 | func TestNewFileHandler_basic(t *testing.T) { 59 | testFile := "testdata/file-basic.log" 60 | assert.NoErr(t, fsutil.DeleteIfFileExist(testFile)) 61 | 62 | h, err := handler.NewFileHandler(testFile) 63 | assert.NoErr(t, err) 64 | assert.NotEmpty(t, h.Writer()) 65 | 66 | r := newLogRecord("test file handler") 67 | 68 | err = h.Handle(r) 69 | assert.NoErr(t, err) 70 | assert.NoErr(t, h.Close()) 71 | 72 | bts := fsutil.MustReadFile(testFile) 73 | str := string(bts) 74 | 75 | assert.Contains(t, str, `INFO`) 76 | assert.Contains(t, str, `test file handler`) 77 | } 78 | 79 | func TestNewBuffFileHandler(t *testing.T) { 80 | testFile := "testdata/file-buff.log" 81 | assert.NoErr(t, fsutil.DeleteIfFileExist(testFile)) 82 | 83 | h, err := handler.NewBuffFileHandler(testFile, 56) 84 | assert.NoErr(t, err) 85 | assert.NotEmpty(t, h.Writer()) 86 | 87 | r := newLogRecord("test file buff handler") 88 | 89 | err = h.Handle(r) 90 | assert.NoErr(t, err) 91 | assert.NoErr(t, h.Close()) 92 | 93 | bts := fsutil.MustReadFile(testFile) 94 | str := string(bts) 95 | 96 | assert.Contains(t, str, `INFO`) 97 | assert.Contains(t, str, `test file buff handler`) 98 | } 99 | 100 | func TestJSONFileHandler(t *testing.T) { 101 | testFile := "testdata/file-json.log" 102 | assert.NoErr(t, fsutil.DeleteIfFileExist(testFile)) 103 | 104 | h, err := handler.JSONFileHandler(testFile) 105 | assert.NoErr(t, err) 106 | 107 | r := newLogRecord("test json file handler") 108 | err = h.Handle(r) 109 | assert.NoErr(t, err) 110 | 111 | err = h.Close() 112 | assert.NoErr(t, err) 113 | 114 | bts := fsutil.MustReadFile(testFile) 115 | str := string(bts) 116 | 117 | assert.Contains(t, str, `"level":"INFO"`) 118 | assert.Contains(t, str, `"message":"test json file handler"`) 119 | } 120 | 121 | func TestMustSimpleFile(t *testing.T) { 122 | logfile := "./testdata/must-simple-file.log" 123 | assert.NoErr(t, fsutil.DeleteIfFileExist(logfile)) 124 | 125 | h := handler.MustSimpleFile(logfile) 126 | assert.True(t, h.IsHandling(slog.InfoLevel)) 127 | } 128 | 129 | func TestNewSimpleFileHandler(t *testing.T) { 130 | logfile := "./testdata/simple-file.log" 131 | assert.NoErr(t, fsutil.DeleteIfFileExist(logfile)) 132 | assert.False(t, fsutil.IsFile(logfile)) 133 | 134 | h, err := handler.NewSimpleFileHandler(logfile) 135 | assert.NoErr(t, err) 136 | 137 | l := slog.NewWithHandlers(h) 138 | l.Info("info message") 139 | l.Warn("warn message") 140 | 141 | assert.True(t, fsutil.IsFile(logfile)) 142 | // assert.NoErr(t, os.Remove(logfile)) 143 | bts, err := os.ReadFile(logfile) 144 | assert.NoErr(t, err) 145 | 146 | str := string(bts) 147 | assert.Contains(t, str, "[INFO]") 148 | assert.Contains(t, str, slog.WarnLevel.Name()) 149 | } 150 | -------------------------------------------------------------------------------- /handler/handler.go: -------------------------------------------------------------------------------- 1 | // Package handler provide useful common log handlers. 2 | // 3 | // eg: file, console, multi_file, rotate_file, stream, syslog, email 4 | package handler 5 | 6 | import ( 7 | "io" 8 | "os" 9 | "sync" 10 | 11 | "github.com/gookit/goutil/fsutil" 12 | "github.com/gookit/slog" 13 | ) 14 | 15 | // DefaultBufferSize sizes the buffer associated with each log file. It's large 16 | // so that log records can accumulate without the logging thread blocking 17 | // on disk I/O. The flushDaemon will block instead. 18 | var DefaultBufferSize = 8 * 1024 19 | 20 | var ( 21 | // DefaultFilePerm perm and flags for create log file 22 | DefaultFilePerm os.FileMode = 0664 23 | // DefaultFileFlags for create/open file 24 | DefaultFileFlags = os.O_CREATE | os.O_WRONLY | os.O_APPEND 25 | ) 26 | 27 | // FlushWriter is the interface satisfied by logging destinations. 28 | type FlushWriter interface { 29 | Flush() error 30 | // Writer the output writer 31 | io.Writer 32 | } 33 | 34 | // FlushCloseWriter is the interface satisfied by logging destinations. 35 | type FlushCloseWriter interface { 36 | Flush() error 37 | // WriteCloser the output writer 38 | io.WriteCloser 39 | } 40 | 41 | // SyncCloseWriter is the interface satisfied by logging destinations. 42 | // such as os.File 43 | type SyncCloseWriter interface { 44 | Sync() error 45 | // WriteCloser the output writer 46 | io.WriteCloser 47 | } 48 | 49 | /******************************************************************************** 50 | * Common parts for handler 51 | ********************************************************************************/ 52 | 53 | // LevelWithFormatter struct definition 54 | // 55 | // - support set log formatter 56 | // - only support set one log level 57 | // 58 | // Deprecated: please use slog.LevelWithFormatter instead. 59 | type LevelWithFormatter = slog.LevelWithFormatter 60 | 61 | // LevelsWithFormatter struct definition 62 | // 63 | // - support set log formatter 64 | // - support setting multi log levels 65 | // 66 | // Deprecated: please use slog.LevelsWithFormatter instead. 67 | type LevelsWithFormatter = slog.LevelsWithFormatter 68 | 69 | // NopFlushClose no operation. 70 | // 71 | // provide empty Flush(), Close() methods, useful for tests. 72 | type NopFlushClose struct{} 73 | 74 | // Flush logs to disk 75 | func (h *NopFlushClose) Flush() error { 76 | return nil 77 | } 78 | 79 | // Close handler 80 | func (h *NopFlushClose) Close() error { 81 | return nil 82 | } 83 | 84 | // LockWrapper struct 85 | type LockWrapper struct { 86 | sync.Mutex 87 | disable bool 88 | } 89 | 90 | // Lock it 91 | func (lw *LockWrapper) Lock() { 92 | if !lw.disable { 93 | lw.Mutex.Lock() 94 | } 95 | } 96 | 97 | // Unlock it 98 | func (lw *LockWrapper) Unlock() { 99 | if !lw.disable { 100 | lw.Mutex.Unlock() 101 | } 102 | } 103 | 104 | // EnableLock enable lock 105 | func (lw *LockWrapper) EnableLock(enable bool) { 106 | lw.disable = !enable 107 | } 108 | 109 | // LockEnabled status 110 | func (lw *LockWrapper) LockEnabled() bool { 111 | return !lw.disable 112 | } 113 | 114 | // QuickOpenFile like os.OpenFile 115 | func QuickOpenFile(filepath string) (*os.File, error) { 116 | return fsutil.OpenFile(filepath, DefaultFileFlags, DefaultFilePerm) 117 | } 118 | -------------------------------------------------------------------------------- /handler/handler_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/goutil" 8 | "github.com/gookit/goutil/errorx" 9 | "github.com/gookit/goutil/fsutil" 10 | "github.com/gookit/goutil/testutil/assert" 11 | "github.com/gookit/slog" 12 | "github.com/gookit/slog/handler" 13 | ) 14 | 15 | var ( 16 | sampleData = slog.M{ 17 | "name": "inhere", 18 | "age": 100, 19 | "skill": "go,php,java", 20 | } 21 | ) 22 | 23 | func TestMain(m *testing.M) { 24 | fmt.Println("TestMain: remove all test files in ./testdata") 25 | goutil.PanicErr(fsutil.RemoveSub("./testdata", fsutil.ExcludeNames(".keep"))) 26 | m.Run() 27 | } 28 | 29 | func TestConfig_CreateWriter(t *testing.T) { 30 | cfg := handler.NewEmptyConfig() 31 | 32 | w, err := cfg.CreateWriter() 33 | assert.Nil(t, w) 34 | assert.Err(t, err) 35 | 36 | h, err := cfg.CreateHandler() 37 | assert.Nil(t, h) 38 | assert.Err(t, err) 39 | 40 | logfile := "./testdata/file-by-config.log" 41 | assert.NoErr(t, fsutil.DeleteIfFileExist(logfile)) 42 | 43 | cfg.With( 44 | handler.WithBuffMode(handler.BuffModeBite), 45 | handler.WithLogLevels(slog.NormalLevels), 46 | handler.WithLogfile(logfile), 47 | ) 48 | 49 | w, err = cfg.CreateWriter() 50 | assert.NoErr(t, err) 51 | 52 | _, err = w.Write([]byte("hello, config")) 53 | assert.NoErr(t, err) 54 | 55 | bts := fsutil.MustReadFile(logfile) 56 | str := string(bts) 57 | 58 | assert.Eq(t, str, "hello, config") 59 | assert.NoErr(t, w.Sync()) 60 | assert.NoErr(t, w.Close()) 61 | } 62 | 63 | func TestConfig_RotateWriter(t *testing.T) { 64 | cfg := handler.NewEmptyConfig() 65 | 66 | w, err := cfg.RotateWriter() 67 | assert.Nil(t, w) 68 | assert.Err(t, err) 69 | } 70 | 71 | func TestConsoleHandlerWithColor(t *testing.T) { 72 | l := slog.NewWithHandlers(handler.ConsoleWithLevels(slog.AllLevels)) 73 | l.DoNothingOnPanicFatal() 74 | l.Configure(func(l *slog.Logger) { 75 | l.ReportCaller = true 76 | }) 77 | 78 | logAllLevel(l, "this is a simple log message") 79 | // logfAllLevel() 80 | } 81 | 82 | func TestConsoleHandlerNoColor(t *testing.T) { 83 | h := handler.NewConsole(slog.AllLevels) 84 | // no color 85 | h.TextFormatter().EnableColor = false 86 | 87 | l := slog.NewWithHandlers(h) 88 | l.DoNothingOnPanicFatal() 89 | l.ReportCaller = true 90 | 91 | logAllLevel(l, "this is a simple log message") 92 | } 93 | 94 | func TestNewEmailHandler(t *testing.T) { 95 | from := handler.EmailOption{ 96 | SMTPHost: "smtp.gmail.com", 97 | SMTPPort: 587, 98 | FromAddr: "someone@gmail.com", 99 | } 100 | 101 | h := handler.NewEmailHandler(from, []string{ 102 | "another@gmail.com", 103 | }) 104 | 105 | assert.Eq(t, slog.InfoLevel, h.Level) 106 | 107 | // handle error 108 | h.SetFormatter(newTestFormatter(true)) 109 | assert.Err(t, h.Handle(newLogRecord("test email handler"))) 110 | } 111 | 112 | func TestLevelWithFormatter(t *testing.T) { 113 | lf := handler.LevelWithFormatter{Level: slog.InfoLevel} 114 | 115 | assert.True(t, lf.IsHandling(slog.ErrorLevel)) 116 | assert.True(t, lf.IsHandling(slog.InfoLevel)) 117 | assert.False(t, lf.IsHandling(slog.DebugLevel)) 118 | } 119 | 120 | func TestLevelsWithFormatter(t *testing.T) { 121 | lsf := handler.LevelsWithFormatter{Levels: slog.NormalLevels} 122 | 123 | assert.False(t, lsf.IsHandling(slog.ErrorLevel)) 124 | assert.True(t, lsf.IsHandling(slog.InfoLevel)) 125 | assert.True(t, lsf.IsHandling(slog.DebugLevel)) 126 | } 127 | 128 | func TestNopFlushClose_Flush(t *testing.T) { 129 | nfc := handler.NopFlushClose{} 130 | 131 | assert.NoErr(t, nfc.Flush()) 132 | assert.NoErr(t, nfc.Close()) 133 | } 134 | 135 | func TestLockWrapper_Lock(t *testing.T) { 136 | lw := &handler.LockWrapper{} 137 | assert.True(t, lw.LockEnabled()) 138 | 139 | lw.EnableLock(true) 140 | assert.True(t, lw.LockEnabled()) 141 | 142 | a := 1 143 | lw.Lock() 144 | a++ 145 | lw.Unlock() 146 | assert.Eq(t, 2, a) 147 | } 148 | 149 | func logAllLevel(log slog.SLogger, msg string) { 150 | for _, level := range slog.AllLevels { 151 | log.Log(level, msg) 152 | } 153 | } 154 | 155 | func newLogRecord(msg string) *slog.Record { 156 | r := &slog.Record{ 157 | Channel: "handler_test", 158 | Level: slog.InfoLevel, 159 | Message: msg, 160 | Time: slog.DefaultClockFn.Now(), 161 | Data: sampleData, 162 | Extra: map[string]any{ 163 | "source": "linux", 164 | "extra_key0": "hello", 165 | "sub": slog.M{"sub_key1": "val0"}, 166 | }, 167 | } 168 | 169 | r.Init(false) 170 | return r 171 | } 172 | 173 | type testHandler struct { 174 | errOnHandle bool 175 | errOnFlush bool 176 | errOnClose bool 177 | } 178 | 179 | func newTestHandler() *testHandler { 180 | return &testHandler{} 181 | } 182 | 183 | // func (h testHandler) Reset() { 184 | // h.errOnHandle = false 185 | // h.errOnFlush = false 186 | // h.errOnClose = false 187 | // } 188 | 189 | func (h testHandler) IsHandling(_ slog.Level) bool { 190 | return true 191 | } 192 | 193 | func (h testHandler) Close() error { 194 | if h.errOnClose { 195 | return errorx.Raw("close error") 196 | } 197 | return nil 198 | } 199 | 200 | func (h testHandler) Flush() error { 201 | if h.errOnFlush { 202 | return errorx.Raw("flush error") 203 | } 204 | return nil 205 | } 206 | 207 | func (h testHandler) Handle(_ *slog.Record) error { 208 | if h.errOnHandle { 209 | return errorx.Raw("handle error") 210 | } 211 | return nil 212 | } 213 | 214 | type testFormatter struct { 215 | errOnFormat bool 216 | } 217 | 218 | func newTestFormatter(errOnFormat ...bool) *testFormatter { 219 | return &testFormatter{ 220 | errOnFormat: len(errOnFormat) > 0 && errOnFormat[0], 221 | } 222 | } 223 | 224 | func (f testFormatter) Format(r *slog.Record) ([]byte, error) { 225 | if f.errOnFormat { 226 | return nil, errorx.Raw("format error") 227 | } 228 | return []byte(r.Message), nil 229 | } 230 | -------------------------------------------------------------------------------- /handler/rotatefile.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gookit/goutil/basefn" 5 | "github.com/gookit/slog/rotatefile" 6 | ) 7 | 8 | // NewRotateFileHandler instance. It supports splitting log files by time and size 9 | func NewRotateFileHandler(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) (*SyncCloseHandler, error) { 10 | cfg := NewConfig(fns...).With(WithLogfile(logfile), WithRotateTime(rt)) 11 | 12 | writer, err := cfg.RotateWriter() 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | h := NewSyncCloseHandler(writer, cfg.Levels) 18 | return h, nil 19 | } 20 | 21 | // MustRotateFile handler instance, will panic on create error 22 | func MustRotateFile(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) *SyncCloseHandler { 23 | return basefn.Must(NewRotateFileHandler(logfile, rt, fns...)) 24 | } 25 | 26 | // NewRotateFile instance. alias of NewRotateFileHandler() 27 | func NewRotateFile(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) (*SyncCloseHandler, error) { 28 | return NewRotateFileHandler(logfile, rt, fns...) 29 | } 30 | 31 | // 32 | // --------------------------------------------------------------------------- 33 | // rotate file by size 34 | // --------------------------------------------------------------------------- 35 | // 36 | 37 | // MustSizeRotateFile instance 38 | func MustSizeRotateFile(logfile string, maxSize int, fns ...ConfigFn) *SyncCloseHandler { 39 | return basefn.Must(NewSizeRotateFileHandler(logfile, maxSize, fns...)) 40 | } 41 | 42 | // NewSizeRotateFile instance 43 | func NewSizeRotateFile(logfile string, maxSize int, fns ...ConfigFn) (*SyncCloseHandler, error) { 44 | return NewSizeRotateFileHandler(logfile, maxSize, fns...) 45 | } 46 | 47 | // NewSizeRotateFileHandler instance, default close rotate by time. 48 | func NewSizeRotateFileHandler(logfile string, maxSize int, fns ...ConfigFn) (*SyncCloseHandler, error) { 49 | // close rotate by time. 50 | fns = append(fns, WithMaxSize(uint64(maxSize))) 51 | return NewRotateFileHandler(logfile, 0, fns...) 52 | } 53 | 54 | // 55 | // --------------------------------------------------------------------------- 56 | // rotate log file by time 57 | // --------------------------------------------------------------------------- 58 | // 59 | 60 | // RotateTime rotate log file by time. 61 | // 62 | // EveryDay: 63 | // - "error.log.20201223" 64 | // 65 | // EveryHour, Every30Minutes, EveryMinute: 66 | // - "error.log.20201223_1500" 67 | // - "error.log.20201223_1530" 68 | // - "error.log.20201223_1523" 69 | // 70 | // Deprecated: please use rotatefile.RotateTime 71 | type RotateTime = rotatefile.RotateTime 72 | 73 | // Deprecated: Please use define constants on pkg rotatefile. e.g. rotatefile.EveryDay 74 | const ( 75 | EveryDay = rotatefile.EveryDay 76 | EveryHour = rotatefile.EveryDay 77 | 78 | Every30Minutes = rotatefile.Every30Min 79 | Every15Minutes = rotatefile.Every15Min 80 | 81 | EveryMinute = rotatefile.EveryMinute 82 | EverySecond = rotatefile.EverySecond // only use for tests 83 | ) 84 | 85 | // MustTimeRotateFile instance 86 | func MustTimeRotateFile(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) *SyncCloseHandler { 87 | return basefn.Must(NewTimeRotateFileHandler(logfile, rt, fns...)) 88 | } 89 | 90 | // NewTimeRotateFile instance 91 | func NewTimeRotateFile(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) (*SyncCloseHandler, error) { 92 | return NewTimeRotateFileHandler(logfile, rt, fns...) 93 | } 94 | 95 | // NewTimeRotateFileHandler instance, default close rotate by size 96 | func NewTimeRotateFileHandler(logfile string, rt rotatefile.RotateTime, fns ...ConfigFn) (*SyncCloseHandler, error) { 97 | // default close rotate by size: WithMaxSize(0) 98 | return NewRotateFileHandler(logfile, rt, append(fns, WithMaxSize(0))...) 99 | } 100 | -------------------------------------------------------------------------------- /handler/rotatefile_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/gookit/goutil/fsutil" 10 | "github.com/gookit/goutil/testutil/assert" 11 | "github.com/gookit/goutil/timex" 12 | "github.com/gookit/slog" 13 | "github.com/gookit/slog/handler" 14 | "github.com/gookit/slog/internal" 15 | "github.com/gookit/slog/rotatefile" 16 | ) 17 | 18 | func TestNewRotateFileHandler(t *testing.T) { 19 | // by size 20 | logfile := "./testdata/both-rotate-bysize.log" 21 | assert.NoErr(t, fsutil.DeleteIfFileExist(logfile)) 22 | 23 | h, err := handler.NewRotateFile(logfile, handler.EveryMinute, handler.WithMaxSize(128)) 24 | assert.NoErr(t, err) 25 | assert.True(t, fsutil.IsFile(logfile)) 26 | 27 | l := slog.NewWithHandlers(h) 28 | l.ReportCaller = true 29 | 30 | for i := 0; i < 3; i++ { 31 | l.Info("info", "message", i) 32 | l.Warn("warn message", i) 33 | } 34 | l.MustClose() 35 | 36 | // by time 37 | logfile = "./testdata/both-rotate-bytime.log" 38 | assert.NoErr(t, fsutil.DeleteIfFileExist(logfile)) 39 | 40 | h = handler.MustRotateFile(logfile, handler.EverySecond) 41 | assert.True(t, fsutil.IsFile(logfile)) 42 | 43 | l = slog.NewWithHandlers(h) 44 | 45 | for i := 0; i < 3; i++ { 46 | l.Info("info", "message", i) 47 | l.Warn("warn message", i) 48 | fmt.Println("second ", i+1) 49 | time.Sleep(time.Second * 1) 50 | } 51 | l.Error("error message") 52 | 53 | assert.NoErr(t, l.FlushAll()) 54 | } 55 | 56 | func TestNewSizeRotateFileHandler(t *testing.T) { 57 | t.Run("NewSizeRotateFile", func(t *testing.T) { 58 | logfile := "./testdata/size-rotate-file.log" 59 | assert.NoErr(t, fsutil.DeleteIfFileExist(logfile)) 60 | 61 | h, err := handler.NewSizeRotateFile(logfile, 468, handler.WithBuffSize(256)) 62 | assert.NoErr(t, err) 63 | assert.True(t, fsutil.IsFile(logfile)) 64 | 65 | l := slog.NewWithHandlers(h) 66 | l.ReportCaller = true 67 | l.CallerFlag = slog.CallerFlagFull 68 | 69 | for i := 0; i < 4; i++ { 70 | l.Info("this is a info", "message, index=", i) 71 | l.Warn("this is a warning message, index=", i) 72 | } 73 | 74 | assert.NoErr(t, l.Close()) 75 | checkLogFileContents(t, logfile) 76 | }) 77 | 78 | t.Run("MustSizeRotateFile", func(t *testing.T) { 79 | logfile := "./testdata/must-size-rotate-file.log" 80 | h := handler.MustSizeRotateFile(logfile, 128, handler.WithBuffSize(128)) 81 | h.SetFormatter(slog.NewJSONFormatter()) 82 | err := h.Handle(newLogRecord("this is a info message")) 83 | assert.NoErr(t, err) 84 | 85 | files := fsutil.Glob(internal.BuildGlobPattern(logfile)) 86 | assert.Len(t, files, 2) 87 | }) 88 | } 89 | 90 | func TestNewTimeRotateFileHandler_EveryDay(t *testing.T) { 91 | logfile := "./testdata/time-rotate_EveryDay.log" 92 | newFile := internal.AddSuffix2path(logfile, "20221116") 93 | 94 | clock := rotatefile.NewMockClock("2022-11-16 23:59:57") 95 | options := []handler.ConfigFn{ 96 | handler.WithBuffSize(128), 97 | handler.WithTimeClock(clock), 98 | } 99 | 100 | h := handler.MustTimeRotateFile(logfile, handler.EveryDay, options...) 101 | assert.True(t, fsutil.IsFile(logfile)) 102 | 103 | l := slog.NewWithHandlers(h) 104 | l.ReportCaller = true 105 | l.TimeClock = clock.Now 106 | 107 | for i := 0; i < 6; i++ { 108 | l.WithData(sampleData).Info("the th:", i, "info message") 109 | l.Warnf("the th:%d warning message text", i) 110 | fmt.Println("log number ", (i+1)*2) 111 | clock.Add(time.Second * 1) 112 | } 113 | 114 | l.MustClose() 115 | checkLogFileContents(t, logfile) 116 | checkLogFileContents(t, newFile) 117 | } 118 | 119 | func TestNewTimeRotateFileHandler_EveryHour(t *testing.T) { 120 | clock := rotatefile.NewMockClock("2022-04-28 20:59:58") 121 | logfile := "./testdata/time-rotate_EveryHour.log" 122 | newFile := internal.AddSuffix2path(logfile, timex.DateFormat(clock.Now(), "Ymd_H00")) 123 | 124 | options := []handler.ConfigFn{ 125 | handler.WithTimeClock(clock), 126 | handler.WithBuffSize(0), 127 | } 128 | h, err := handler.NewTimeRotateFile(logfile, rotatefile.EveryHour, options...) 129 | 130 | assert.NoErr(t, err) 131 | assert.True(t, fsutil.IsFile(logfile)) 132 | 133 | l := slog.NewWithHandlers(h) 134 | l.ReportCaller = true 135 | l.TimeClock = clock.Now 136 | 137 | for i := 0; i < 6; i++ { 138 | l.WithData(sampleData).Info("the th:", i, "info message") 139 | l.Warnf("the th:%d warning message text", i) 140 | fmt.Println("log number ", (i+1)*2) 141 | clock.Add(time.Second * 1) 142 | } 143 | l.MustClose() 144 | 145 | checkLogFileContents(t, logfile) 146 | checkLogFileContents(t, newFile) 147 | } 148 | 149 | func TestNewTimeRotateFileHandler_someSeconds(t *testing.T) { 150 | logfile := "./testdata/time-rotate-Seconds.log" 151 | assert.NoErr(t, fsutil.DeleteIfExist(logfile)) 152 | h, err := handler.NewTimeRotateFileHandler(logfile, handler.EverySecond) 153 | 154 | assert.NoErr(t, err) 155 | assert.True(t, fsutil.IsFile(logfile)) 156 | 157 | l := slog.NewWithHandlers(h) 158 | l.ReportCaller = true 159 | 160 | for i := 0; i < 3; i++ { 161 | l.Info("info", "message", i) 162 | l.Warn("warning message", i) 163 | fmt.Println("second ", i+1) 164 | time.Sleep(time.Second * 1) 165 | } 166 | l.MustClose() 167 | // assert.NoErr(t, os.Remove(fpath)) 168 | } 169 | 170 | func checkLogFileContents(t *testing.T, logfile string) { 171 | assert.True(t, fsutil.IsFile(logfile)) 172 | 173 | bts, err := os.ReadFile(logfile) 174 | assert.NoErr(t, err) 175 | 176 | str := string(bts) 177 | assert.Contains(t, str, "[INFO]") 178 | assert.Contains(t, str, "info message") 179 | assert.Contains(t, str, "[WARNING]") 180 | assert.Contains(t, str, "warning message") 181 | } 182 | -------------------------------------------------------------------------------- /handler/syslog.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !plan9 2 | 3 | package handler 4 | 5 | import ( 6 | "log/syslog" 7 | 8 | "github.com/gookit/slog" 9 | ) 10 | 11 | // SysLogOpt for syslog handler 12 | type SysLogOpt struct { 13 | // Tag syslog tag 14 | Tag string 15 | // Priority syslog priority 16 | Priority syslog.Priority 17 | // Network syslog network 18 | Network string 19 | // Raddr syslog address 20 | Raddr string 21 | } 22 | 23 | // SysLogHandler struct 24 | type SysLogHandler struct { 25 | slog.LevelWithFormatter 26 | writer *syslog.Writer 27 | } 28 | 29 | // NewSysLogHandler instance 30 | func NewSysLogHandler(priority syslog.Priority, tag string) (*SysLogHandler, error) { 31 | return NewSysLog(&SysLogOpt{ 32 | Priority: priority, 33 | Tag: tag, 34 | }) 35 | } 36 | 37 | // NewSysLog handler instance with all custom options. 38 | func NewSysLog(opt *SysLogOpt) (*SysLogHandler, error) { 39 | slWriter, err := syslog.Dial(opt.Network, opt.Raddr, opt.Priority, opt.Tag) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | h := &SysLogHandler{ 45 | writer: slWriter, 46 | } 47 | 48 | // init default log level 49 | h.Level = slog.InfoLevel 50 | return h, nil 51 | } 52 | 53 | // Handle a log record 54 | func (h *SysLogHandler) Handle(record *slog.Record) error { 55 | bts, err := h.Formatter().Format(record) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | s := string(bts) 61 | 62 | // write log by level 63 | switch record.Level { 64 | case slog.DebugLevel, slog.TraceLevel: 65 | return h.writer.Debug(s) 66 | case slog.NoticeLevel: 67 | return h.writer.Notice(s) 68 | case slog.WarnLevel: 69 | return h.writer.Warning(s) 70 | case slog.ErrorLevel: 71 | return h.writer.Err(s) 72 | case slog.FatalLevel: 73 | return h.writer.Crit(s) 74 | case slog.PanicLevel: 75 | return h.writer.Emerg(s) 76 | default: // as info level 77 | return h.writer.Info(s) 78 | } 79 | } 80 | 81 | // Close handler 82 | func (h *SysLogHandler) Close() error { 83 | return h.writer.Close() 84 | } 85 | 86 | // Flush handler 87 | func (h *SysLogHandler) Flush() error { 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /handler/syslog_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !plan9 2 | 3 | package handler_test 4 | 5 | import ( 6 | "log/syslog" 7 | "testing" 8 | 9 | "github.com/gookit/goutil/testutil/assert" 10 | "github.com/gookit/slog/handler" 11 | ) 12 | 13 | func TestNewSysLogHandler(t *testing.T) { 14 | h, err := handler.NewSysLogHandler(syslog.LOG_INFO, "slog") 15 | assert.NoErr(t, err) 16 | 17 | err = h.Handle(newLogRecord("test syslog handler")) 18 | assert.NoErr(t, err) 19 | 20 | assert.NoErr(t, h.Flush()) 21 | assert.NoErr(t, h.Close()) 22 | } 23 | -------------------------------------------------------------------------------- /handler/testdata/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/slog/90c4be86a3060e094e279af7009139357eaf41a5/handler/testdata/.keep -------------------------------------------------------------------------------- /handler/write_close_flusher.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gookit/slog" 5 | ) 6 | 7 | // FlushCloseHandler definition 8 | type FlushCloseHandler struct { 9 | slog.LevelFormattable 10 | Output FlushCloseWriter 11 | } 12 | 13 | // NewFlushCloserWithLF create new FlushCloseHandler, with custom slog.LevelFormattable 14 | func NewFlushCloserWithLF(out FlushCloseWriter, lf slog.LevelFormattable) *FlushCloseHandler { 15 | return &FlushCloseHandler{ 16 | Output: out, 17 | // init formatter and level handle 18 | LevelFormattable: lf, 19 | } 20 | } 21 | 22 | // 23 | // ------------- Use max log level ------------- 24 | // 25 | 26 | // FlushCloserWithMaxLevel create new FlushCloseHandler, with max log level 27 | func FlushCloserWithMaxLevel(out FlushCloseWriter, maxLevel slog.Level) *FlushCloseHandler { 28 | return NewFlushCloserWithLF(out, slog.NewLvFormatter(maxLevel)) 29 | } 30 | 31 | // 32 | // ------------- Use multi log levels ------------- 33 | // 34 | 35 | // NewFlushCloser create new FlushCloseHandler, alias of NewFlushCloseHandler() 36 | func NewFlushCloser(out FlushCloseWriter, levels []slog.Level) *FlushCloseHandler { 37 | return NewFlushCloseHandler(out, levels) 38 | } 39 | 40 | // FlushCloserWithLevels create new FlushCloseHandler, alias of NewFlushCloseHandler() 41 | func FlushCloserWithLevels(out FlushCloseWriter, levels []slog.Level) *FlushCloseHandler { 42 | return NewFlushCloseHandler(out, levels) 43 | } 44 | 45 | // NewFlushCloseHandler create new FlushCloseHandler 46 | // 47 | // Usage: 48 | // 49 | // buf := new(byteutil.Buffer) 50 | // h := handler.NewFlushCloseHandler(&buf, slog.AllLevels) 51 | // 52 | // f, err := os.OpenFile("my.log", ...) 53 | // h := handler.NewFlushCloseHandler(f, slog.AllLevels) 54 | func NewFlushCloseHandler(out FlushCloseWriter, levels []slog.Level) *FlushCloseHandler { 55 | return NewFlushCloserWithLF(out, slog.NewLvsFormatter(levels)) 56 | } 57 | 58 | // Close the handler 59 | func (h *FlushCloseHandler) Close() error { 60 | if err := h.Flush(); err != nil { 61 | return err 62 | } 63 | return h.Output.Close() 64 | } 65 | 66 | // Flush the handler 67 | func (h *FlushCloseHandler) Flush() error { 68 | return h.Output.Flush() 69 | } 70 | 71 | // Handle log record 72 | func (h *FlushCloseHandler) Handle(record *slog.Record) error { 73 | bts, err := h.Formatter().Format(record) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | _, err = h.Output.Write(bts) 79 | return err 80 | } 81 | -------------------------------------------------------------------------------- /handler/write_close_syncer.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/gookit/slog" 7 | ) 8 | 9 | // SyncCloseHandler definition 10 | type SyncCloseHandler struct { 11 | slog.LevelFormattable 12 | Output SyncCloseWriter 13 | } 14 | 15 | // NewSyncCloserWithLF create new SyncCloseHandler, with custom slog.LevelFormattable 16 | func NewSyncCloserWithLF(out SyncCloseWriter, lf slog.LevelFormattable) *SyncCloseHandler { 17 | return &SyncCloseHandler{ 18 | Output: out, 19 | // init formatter and level handle 20 | LevelFormattable: lf, 21 | } 22 | } 23 | 24 | // 25 | // ------------- Use max log level ------------- 26 | // 27 | 28 | // SyncCloserWithMaxLevel create new SyncCloseHandler, with max log level 29 | func SyncCloserWithMaxLevel(out SyncCloseWriter, maxLevel slog.Level) *SyncCloseHandler { 30 | return NewSyncCloserWithLF(out, slog.NewLvFormatter(maxLevel)) 31 | } 32 | 33 | // 34 | // ------------- Use multi log levels ------------- 35 | // 36 | 37 | // NewSyncCloser create new SyncCloseHandler, alias of NewSyncCloseHandler() 38 | func NewSyncCloser(out SyncCloseWriter, levels []slog.Level) *SyncCloseHandler { 39 | return NewSyncCloseHandler(out, levels) 40 | } 41 | 42 | // SyncCloserWithLevels create new SyncCloseHandler, alias of NewSyncCloseHandler() 43 | func SyncCloserWithLevels(out SyncCloseWriter, levels []slog.Level) *SyncCloseHandler { 44 | return NewSyncCloseHandler(out, levels) 45 | } 46 | 47 | // NewSyncCloseHandler create new SyncCloseHandler with limited log levels 48 | // 49 | // Usage: 50 | // 51 | // f, err := os.OpenFile("my.log", ...) 52 | // h := handler.NewSyncCloseHandler(f, slog.AllLevels) 53 | func NewSyncCloseHandler(out SyncCloseWriter, levels []slog.Level) *SyncCloseHandler { 54 | return NewSyncCloserWithLF(out, slog.NewLvsFormatter(levels)) 55 | } 56 | 57 | // Close the handler 58 | func (h *SyncCloseHandler) Close() error { 59 | if err := h.Flush(); err != nil { 60 | return err 61 | } 62 | return h.Output.Close() 63 | } 64 | 65 | // Flush the handler 66 | func (h *SyncCloseHandler) Flush() error { 67 | return h.Output.Sync() 68 | } 69 | 70 | // Writer of the handler 71 | func (h *SyncCloseHandler) Writer() io.Writer { 72 | return h.Output 73 | } 74 | 75 | // Handle log record 76 | func (h *SyncCloseHandler) Handle(record *slog.Record) error { 77 | bts, err := h.Formatter().Format(record) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | _, err = h.Output.Write(bts) 83 | return err 84 | } 85 | -------------------------------------------------------------------------------- /handler/write_closer.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/gookit/slog" 7 | ) 8 | 9 | // WriteCloserHandler definition 10 | type WriteCloserHandler struct { 11 | slog.LevelFormattable 12 | Output io.WriteCloser 13 | } 14 | 15 | // NewWriteCloserWithLF create new WriteCloserHandler and with custom slog.LevelFormattable 16 | func NewWriteCloserWithLF(out io.WriteCloser, lf slog.LevelFormattable) *WriteCloserHandler { 17 | return &WriteCloserHandler{ 18 | Output: out, 19 | // init formatter and level handle 20 | LevelFormattable: lf, 21 | } 22 | } 23 | 24 | // WriteCloserWithMaxLevel create new WriteCloserHandler and with max log level 25 | func WriteCloserWithMaxLevel(out io.WriteCloser, maxLevel slog.Level) *WriteCloserHandler { 26 | return NewWriteCloserWithLF(out, slog.NewLvFormatter(maxLevel)) 27 | } 28 | 29 | // 30 | // ------------- Use multi log levels ------------- 31 | // 32 | 33 | // WriteCloserWithLevels create a new instance and with limited log levels 34 | func WriteCloserWithLevels(out io.WriteCloser, levels []slog.Level) *WriteCloserHandler { 35 | // h := &WriteCloserHandler{Output: out} 36 | // h.LimitLevels(levels) 37 | return NewWriteCloserHandler(out, levels) 38 | } 39 | 40 | // NewWriteCloser create a new instance 41 | func NewWriteCloser(out io.WriteCloser, levels []slog.Level) *WriteCloserHandler { 42 | return NewWriteCloserHandler(out, levels) 43 | } 44 | 45 | // NewWriteCloserHandler create new WriteCloserHandler 46 | // 47 | // Usage: 48 | // 49 | // buf := new(bytes.Buffer) 50 | // h := handler.NewIOWriteCloserHandler(&buf, slog.AllLevels) 51 | // 52 | // f, err := os.OpenFile("my.log", ...) 53 | // h := handler.NewIOWriteCloserHandler(f, slog.AllLevels) 54 | func NewWriteCloserHandler(out io.WriteCloser, levels []slog.Level) *WriteCloserHandler { 55 | return NewWriteCloserWithLF(out, slog.NewLvsFormatter(levels)) 56 | } 57 | 58 | // Close the handler 59 | func (h *WriteCloserHandler) Close() error { 60 | return h.Output.Close() 61 | } 62 | 63 | // Flush the handler 64 | func (h *WriteCloserHandler) Flush() error { 65 | return nil 66 | } 67 | 68 | // Handle log record 69 | func (h *WriteCloserHandler) Handle(record *slog.Record) error { 70 | bts, err := h.Formatter().Format(record) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | _, err = h.Output.Write(bts) 76 | return err 77 | } 78 | -------------------------------------------------------------------------------- /handler/writer.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/gookit/slog" 7 | ) 8 | 9 | // IOWriterHandler definition 10 | type IOWriterHandler struct { 11 | NopFlushClose 12 | slog.LevelFormattable 13 | Output io.Writer 14 | } 15 | 16 | // TextFormatter get the formatter 17 | func (h *IOWriterHandler) TextFormatter() *slog.TextFormatter { 18 | return h.Formatter().(*slog.TextFormatter) 19 | } 20 | 21 | // Handle log record 22 | func (h *IOWriterHandler) Handle(record *slog.Record) error { 23 | bts, err := h.Formatter().Format(record) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | _, err = h.Output.Write(bts) 29 | return err 30 | } 31 | 32 | // NewIOWriterWithLF create new IOWriterHandler, with custom slog.LevelFormattable 33 | func NewIOWriterWithLF(out io.Writer, lf slog.LevelFormattable) *IOWriterHandler { 34 | return &IOWriterHandler{ 35 | Output: out, 36 | // init formatter and level handle 37 | LevelFormattable: lf, 38 | } 39 | } 40 | 41 | // 42 | // ------------- Use max log level ------------- 43 | // 44 | 45 | // IOWriterWithMaxLevel create new IOWriterHandler, with max log level 46 | // 47 | // Usage: 48 | // 49 | // buf := new(bytes.Buffer) 50 | // h := handler.IOWriterWithMaxLevel(buf, slog.InfoLevel) 51 | // slog.AddHandler(h) 52 | // slog.Info("info message") 53 | func IOWriterWithMaxLevel(out io.Writer, maxLevel slog.Level) *IOWriterHandler { 54 | return NewIOWriterWithLF(out, slog.NewLvFormatter(maxLevel)) 55 | } 56 | 57 | // 58 | // ------------- Use multi log levels ------------- 59 | // 60 | 61 | // NewIOWriter create a new instance and with limited log levels 62 | func NewIOWriter(out io.Writer, levels []slog.Level) *IOWriterHandler { 63 | return NewIOWriterHandler(out, levels) 64 | } 65 | 66 | // IOWriterWithLevels create a new instance and with limited log levels 67 | func IOWriterWithLevels(out io.Writer, levels []slog.Level) *IOWriterHandler { 68 | return NewIOWriterHandler(out, levels) 69 | } 70 | 71 | // NewIOWriterHandler create new IOWriterHandler 72 | // 73 | // Usage: 74 | // 75 | // buf := new(bytes.Buffer) 76 | // h := handler.NewIOWriterHandler(&buf, slog.AllLevels) 77 | // 78 | // f, err := os.OpenFile("my.log", ...) 79 | // h := handler.NewIOWriterHandler(f, slog.AllLevels) 80 | func NewIOWriterHandler(out io.Writer, levels []slog.Level) *IOWriterHandler { 81 | return NewIOWriterWithLF(out, slog.NewLvsFormatter(levels)) 82 | } 83 | 84 | // SimpleHandler definition. alias of IOWriterHandler 85 | type SimpleHandler = IOWriterHandler 86 | 87 | // NewHandler create a new instance 88 | func NewHandler(out io.Writer, maxLevel slog.Level) *SimpleHandler { 89 | return NewSimpleHandler(out, maxLevel) 90 | } 91 | 92 | // NewSimple create a new instance 93 | func NewSimple(out io.Writer, maxLevel slog.Level) *SimpleHandler { 94 | return NewSimpleHandler(out, maxLevel) 95 | } 96 | 97 | // SimpleWithLevels create new simple handler, with log levels 98 | func SimpleWithLevels(out io.Writer, levels []slog.Level) *IOWriterHandler { 99 | return NewIOWriterHandler(out, levels) 100 | } 101 | 102 | // NewSimpleHandler create new SimpleHandler 103 | // 104 | // Usage: 105 | // 106 | // buf := new(bytes.Buffer) 107 | // h := handler.NewSimpleHandler(&buf, slog.InfoLevel) 108 | // 109 | // f, err := os.OpenFile("my.log", ...) 110 | // h := handler.NewSimpleHandler(f, slog.InfoLevel) 111 | func NewSimpleHandler(out io.Writer, maxLevel slog.Level) *IOWriterHandler { 112 | return IOWriterWithMaxLevel(out, maxLevel) 113 | } 114 | -------------------------------------------------------------------------------- /handler/writer_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/fsutil" 9 | "github.com/gookit/goutil/testutil/assert" 10 | "github.com/gookit/goutil/testutil/fakeobj" 11 | "github.com/gookit/slog" 12 | "github.com/gookit/slog/handler" 13 | ) 14 | 15 | func TestNewIOWriter(t *testing.T) { 16 | w := new(bytes.Buffer) 17 | h := handler.NewIOWriter(w, slog.NormalLevels) 18 | 19 | assert.True(t, h.IsHandling(slog.NoticeLevel)) 20 | 21 | r := newLogRecord("test io.writer handler") 22 | assert.NoErr(t, h.Handle(r)) 23 | assert.NoErr(t, h.Flush()) 24 | 25 | str := w.String() 26 | assert.Contains(t, str, "test io.writer handler") 27 | 28 | assert.NoErr(t, h.Close()) 29 | } 30 | 31 | func TestNewSyncCloser(t *testing.T) { 32 | logfile := "./testdata/sync_closer.log" 33 | 34 | f, err := handler.QuickOpenFile(logfile) 35 | assert.NoErr(t, err) 36 | 37 | h := handler.NewSyncCloser(f, slog.DangerLevels) 38 | 39 | assert.True(t, h.IsHandling(slog.WarnLevel)) 40 | assert.False(t, h.IsHandling(slog.InfoLevel)) 41 | 42 | r := newLogRecord("test sync closer handler") 43 | r.Level = slog.ErrorLevel 44 | 45 | err = h.Handle(r) 46 | assert.NoErr(t, err) 47 | assert.NoErr(t, h.Flush()) 48 | 49 | str := fsutil.ReadString(logfile) 50 | assert.Contains(t, str, "test sync closer handler") 51 | assert.NoErr(t, h.Close()) 52 | 53 | t.Run("err on sync", func(t *testing.T) { 54 | w := &syncCloseWriter{} 55 | w.errOnSync = true 56 | h = handler.SyncCloserWithLevels(w, slog.NormalLevels) 57 | 58 | assert.Err(t, h.Flush()) 59 | assert.Err(t, h.Close()) 60 | }) 61 | 62 | // test handle error 63 | h.SetFormatter(newTestFormatter(true)) 64 | assert.Err(t, h.Handle(r)) 65 | } 66 | 67 | func TestNewWriteCloser(t *testing.T) { 68 | w := fakeobj.NewWriter() 69 | h := handler.NewWriteCloser(w, slog.NormalLevels) 70 | 71 | assert.True(t, h.IsHandling(slog.NoticeLevel)) 72 | 73 | r := newLogRecord("test writeCloser handler") 74 | assert.NoErr(t, h.Handle(r)) 75 | assert.NoErr(t, h.Flush()) 76 | 77 | str := w.String() 78 | assert.Contains(t, str, "test writeCloser handler") 79 | assert.NoErr(t, h.Close()) 80 | 81 | t.Run("use max level", func(t *testing.T) { 82 | h = handler.WriteCloserWithMaxLevel(w, slog.WarnLevel) 83 | r = newLogRecord("test max level") 84 | assert.False(t, h.IsHandling(r.Level)) 85 | 86 | r.Level = slog.ErrorLevel 87 | assert.True(t, h.IsHandling(r.Level)) 88 | }) 89 | 90 | // test handle error 91 | t.Run("handle error", func(t *testing.T) { 92 | h = handler.WriteCloserWithLevels(w, slog.NormalLevels) 93 | h.SetFormatter(newTestFormatter(true)) 94 | assert.Err(t, h.Handle(r)) 95 | }) 96 | } 97 | 98 | func TestNewFlushCloser(t *testing.T) { 99 | w := fakeobj.NewWriter() 100 | h := handler.NewFlushCloser(w, slog.AllLevels) 101 | w.WriteString("before flush\n") 102 | 103 | r := newLogRecord("TestNewFlushCloser") 104 | assert.NoErr(t, h.Handle(r)) 105 | 106 | str := w.ResetGet() 107 | assert.Contains(t, str, "TestNewFlushCloser") 108 | 109 | assert.NoErr(t, h.Flush()) 110 | assert.NoErr(t, h.Close()) 111 | 112 | t.Run("ErrOnFlush", func(t *testing.T) { 113 | w.ErrOnFlush = true 114 | assert.Err(t, h.Flush()) 115 | assert.Err(t, h.Close()) 116 | }) 117 | 118 | t.Run("With max level", func(t *testing.T) { 119 | h = handler.FlushCloserWithMaxLevel(w, slog.WarnLevel) 120 | r = newLogRecord("test max level") 121 | assert.False(t, h.IsHandling(r.Level)) 122 | assert.Empty(t, w.String()) 123 | 124 | r.Level = slog.ErrorLevel 125 | assert.True(t, h.IsHandling(r.Level)) 126 | assert.NoErr(t, h.Handle(r)) 127 | assert.NotEmpty(t, w.String()) 128 | }) 129 | 130 | // test handle error 131 | h = handler.FlushCloserWithMaxLevel(w, slog.WarnLevel) 132 | h.SetFormatter(newTestFormatter(true)) 133 | assert.Err(t, h.Handle(r)) 134 | } 135 | 136 | func TestNewSimpleHandler(t *testing.T) { 137 | buf := fakeobj.NewWriter() 138 | 139 | h := handler.NewSimple(buf, slog.InfoLevel) 140 | r := newLogRecord("test simple handler") 141 | assert.NoErr(t, h.Handle(r)) 142 | 143 | s := buf.String() 144 | buf.Reset() 145 | fmt.Print(s) 146 | assert.Contains(t, s, "test simple handler") 147 | 148 | assert.NoErr(t, h.Flush()) 149 | assert.NoErr(t, h.Close()) 150 | 151 | h = handler.NewHandler(buf, slog.InfoLevel) 152 | r = newLogRecord("test simple handler2") 153 | assert.NoErr(t, h.Handle(r)) 154 | 155 | s = buf.ResetGet() 156 | fmt.Print(s) 157 | assert.Contains(t, s, "test simple handler2") 158 | 159 | assert.NoErr(t, h.Flush()) 160 | assert.NoErr(t, h.Close()) 161 | 162 | h = handler.SimpleWithLevels(buf, slog.NormalLevels) 163 | r = newLogRecord("test simple handler with levels") 164 | assert.NoErr(t, h.Handle(r)) 165 | 166 | s = buf.ResetGet() 167 | fmt.Print(s) 168 | assert.Contains(t, s, "test simple handler with levels") 169 | 170 | // handle error 171 | h.SetFormatter(newTestFormatter(true)) 172 | assert.Err(t, h.Handle(r)) 173 | } 174 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package slog_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/goutil/testutil/assert" 7 | "github.com/gookit/slog" 8 | ) 9 | 10 | func TestNewLvFormatter(t *testing.T) { 11 | lf := slog.NewLvFormatter(slog.InfoLevel) 12 | 13 | assert.True(t, lf.IsHandling(slog.ErrorLevel)) 14 | assert.True(t, lf.IsHandling(slog.InfoLevel)) 15 | assert.False(t, lf.IsHandling(slog.DebugLevel)) 16 | 17 | lf.SetMaxLevel(slog.DebugLevel) 18 | assert.True(t, lf.IsHandling(slog.DebugLevel)) 19 | } 20 | 21 | func TestNewLvsFormatter(t *testing.T) { 22 | lf := slog.NewLvsFormatter([]slog.Level{slog.InfoLevel, slog.ErrorLevel}) 23 | assert.True(t, lf.IsHandling(slog.InfoLevel)) 24 | assert.False(t, lf.IsHandling(slog.DebugLevel)) 25 | 26 | lf.SetLimitLevels([]slog.Level{slog.InfoLevel, slog.ErrorLevel, slog.DebugLevel}) 27 | assert.True(t, lf.IsHandling(slog.DebugLevel)) 28 | } 29 | 30 | func TestLevelFormatting(t *testing.T) { 31 | lf := slog.NewMaxLevelFormatting(slog.InfoLevel) 32 | 33 | assert.True(t, lf.IsHandling(slog.InfoLevel)) 34 | assert.False(t, lf.IsHandling(slog.TraceLevel)) 35 | 36 | // use levels 37 | lf = slog.NewLevelsFormatting([]slog.Level{slog.InfoLevel, slog.ErrorLevel}) 38 | 39 | assert.True(t, lf.IsHandling(slog.InfoLevel)) 40 | assert.True(t, lf.IsHandling(slog.ErrorLevel)) 41 | assert.False(t, lf.IsHandling(slog.TraceLevel)) 42 | 43 | // test level mode 44 | assert.Eq(t, "list", slog.LevelModeList.String()) 45 | assert.Eq(t, "max", slog.LevelModeMax.String()) 46 | assert.Eq(t, "unknown", slog.LevelMode(9).String()) 47 | } 48 | -------------------------------------------------------------------------------- /internal/util.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "path/filepath" 4 | 5 | // AddSuffix2path add suffix to file path. 6 | // 7 | // eg: "/path/to/error.log" => "/path/to/error.{suffix}.log" 8 | func AddSuffix2path(filePath, suffix string) string { 9 | ext := filepath.Ext(filePath) 10 | return filePath[:len(filePath)-len(ext)] + "." + suffix + ext 11 | } 12 | 13 | // BuildGlobPattern builds a glob pattern for the given logfile. NOTE: use for testing only. 14 | func BuildGlobPattern(logfile string) string { 15 | return logfile[:len(logfile)-4] + "*" 16 | } 17 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package slog_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gookit/goutil/dump" 11 | "github.com/gookit/goutil/errorx" 12 | "github.com/gookit/goutil/testutil/assert" 13 | "github.com/gookit/goutil/timex" 14 | "github.com/gookit/slog" 15 | "github.com/gookit/slog/handler" 16 | ) 17 | 18 | func TestLoggerBasic(t *testing.T) { 19 | l := slog.New() 20 | l.SetName("testName") 21 | assert.Eq(t, "testName", l.Name()) 22 | 23 | l = slog.NewWithName("testName") 24 | assert.Eq(t, "testName", l.Name()) 25 | } 26 | 27 | func TestLogger_PushHandler(t *testing.T) { 28 | l := slog.New().Configure(func(l *slog.Logger) { 29 | l.DoNothingOnPanicFatal() 30 | }) 31 | 32 | w1 := new(bytes.Buffer) 33 | h1 := handler.NewIOWriterHandler(w1, slog.DangerLevels) 34 | l.PushHandler(h1) 35 | 36 | w2 := new(bytes.Buffer) 37 | h2 := handler.NewIOWriterHandler(w2, slog.NormalLevels) 38 | l.PushHandlers(h2) 39 | 40 | l.Warning(slog.WarnLevel, "message") 41 | l.Logf(slog.TraceLevel, "%s message", slog.TraceLevel) 42 | 43 | assert.Contains(t, w1.String(), "WARNING message") 44 | assert.Contains(t, w2.String(), "TRACE message") 45 | assert.Contains(t, w2.String(), "TestLogger_PushHandler") 46 | 47 | assert.NoErr(t, l.Sync()) 48 | assert.NoErr(t, l.Flush()) 49 | l.MustFlush() 50 | 51 | assert.NoErr(t, l.Close()) 52 | l.MustClose() 53 | l.Reset() 54 | } 55 | 56 | func TestLogger_ReportCaller(t *testing.T) { 57 | l := slog.NewWithConfig(func(logger *slog.Logger) { 58 | logger.ReportCaller = true 59 | logger.CallerFlag = slog.CallerFlagFnLine 60 | }) 61 | 62 | var buf bytes.Buffer 63 | h := handler.NewIOWriterHandler(&buf, slog.AllLevels) 64 | h.SetFormatter(slog.NewJSONFormatter(func(f *slog.JSONFormatter) { 65 | f.Fields = append(f.Fields, slog.FieldKeyCaller) 66 | })) 67 | 68 | l.AddHandler(h) 69 | l.Info("message") 70 | 71 | str := buf.String() 72 | assert.Contains(t, str, `"caller":"logger_test.go`) 73 | } 74 | 75 | func TestLogger_Log(t *testing.T) { 76 | l := slog.NewWithConfig(func(l *slog.Logger) { 77 | l.ReportCaller = true 78 | l.DoNothingOnPanicFatal() 79 | }) 80 | 81 | l.AddHandler(handler.NewConsoleHandler(slog.AllLevels)) 82 | l.Log(slog.InfoLevel, "a", slog.InfoLevel, "message") 83 | 84 | l.WithField("newKey", "value").Fatalln("a fatal message") 85 | l.WithTime(timex.NowHourStart()).Panicln("a panic message") 86 | } 87 | 88 | func TestLogger_WithContext(t *testing.T) { 89 | var buf bytes.Buffer 90 | h := handler.NewIOWriterHandler(&buf, slog.AllLevels) 91 | 92 | l := newLogger() 93 | l.AddHandlers(h) 94 | 95 | ctx := context.Background() 96 | 97 | r := l.WithCtx(ctx) 98 | r.Info("with context") 99 | 100 | str := buf.String() 101 | assert.Contains(t, str, `with context`) 102 | } 103 | 104 | func TestLogger_panic(t *testing.T) { 105 | h := newTestHandler() 106 | h.errOnFlush = true 107 | 108 | l := slog.NewWithHandlers(h) 109 | 110 | assert.Panics(t, func() { 111 | l.MustFlush() 112 | }) 113 | 114 | err := l.LastErr() 115 | assert.Err(t, err) 116 | assert.Eq(t, "flush error", err.Error()) 117 | 118 | h.errOnClose = true 119 | assert.Panics(t, func() { 120 | l.MustClose() 121 | }) 122 | 123 | err = l.LastErr() 124 | assert.Err(t, err) 125 | assert.Eq(t, "close error", err.Error()) 126 | } 127 | 128 | func TestLogger_error(t *testing.T) { 129 | h := newTestHandler() 130 | l := slog.NewWithHandlers(h) 131 | 132 | err := l.VisitAll(func(h slog.Handler) error { 133 | return errorx.Raw("visit error") 134 | }) 135 | assert.Err(t, err) 136 | assert.Eq(t, "visit error", err.Error()) 137 | 138 | h.errOnClose = true 139 | err = l.Close() 140 | assert.Err(t, err) 141 | assert.Eq(t, "close error", err.Error()) 142 | } 143 | 144 | func TestLogger_panicLevel(t *testing.T) { 145 | w := new(bytes.Buffer) 146 | l := slog.NewWithHandlers(handler.NewIOWriter(w, slog.AllLevels)) 147 | 148 | // assert.PanicsWithValue(t, "slog: panic message", func() { 149 | assert.Panics(t, func() { 150 | l.Panicln("panicln message") 151 | }) 152 | assert.Contains(t, w.String(), "[PANIC]") 153 | assert.Contains(t, w.String(), "panicln message") 154 | 155 | w.Reset() 156 | assert.Panics(t, func() { 157 | l.Panicf("panicf message") 158 | }) 159 | assert.Contains(t, w.String(), "panicf message") 160 | 161 | w.Reset() 162 | assert.Panics(t, func() { 163 | l.Panic("panic message") 164 | }) 165 | assert.Contains(t, w.String(), "panic message") 166 | 167 | assert.NoErr(t, l.FlushAll()) 168 | } 169 | 170 | func TestLogger_log_allLevel(t *testing.T) { 171 | l := slog.NewWithConfig(func(l *slog.Logger) { 172 | l.ReportCaller = true 173 | l.DoNothingOnPanicFatal() 174 | }) 175 | 176 | l.AddHandler(handler.NewConsoleHandler(slog.AllLevels)) 177 | printAllLevelLogs(l, "this a", "log", "message") 178 | } 179 | 180 | func TestLogger_logf_allLevel(t *testing.T) { 181 | l := slog.NewWithConfig(func(l *slog.Logger) { 182 | l.ReportCaller = true 183 | l.CallerFlag = slog.CallerFlagFpLine 184 | l.DoNothingOnPanicFatal() 185 | }) 186 | 187 | l.AddHandler(handler.NewConsoleHandler(slog.AllLevels)) 188 | printfAllLevelLogs(l, "this a log %s", "message") 189 | } 190 | 191 | func TestLogger_write_error(t *testing.T) { 192 | h := newTestHandler() 193 | h.errOnHandle = true 194 | 195 | l := slog.NewWithHandlers(h) 196 | l.Info("a message") 197 | 198 | err := l.LastErr() 199 | assert.Err(t, err) 200 | assert.Eq(t, "handle error", err.Error()) 201 | } 202 | 203 | func TestLogger_option_BackupArgs(t *testing.T) { 204 | l := slog.New(func(l *slog.Logger) { 205 | l.BackupArgs = true 206 | l.CallerFlag = slog.CallerFlagPkgFnl 207 | }) 208 | 209 | buf := new(bytes.Buffer) 210 | l.AddHandler(handler.NewSimple(buf, slog.DebugLevel)) 211 | 212 | r := l.Record() 213 | r.Info("str message1") 214 | assert.NotEmpty(t, r.Args) 215 | r = r.Copy() 216 | r.Infof("fmt %s", "message2") 217 | assert.NotEmpty(t, r.Fmt) 218 | assert.NotEmpty(t, r.Args) 219 | r.WithField("key", "value").Info("field message3") 220 | 221 | s := buf.String() 222 | fmt.Println(s) 223 | assert.StrContains(t, s, "str message1") 224 | assert.StrContains(t, s, "fmt message2") 225 | assert.StrContains(t, s, "field message3") 226 | } 227 | 228 | func TestLogger_FlushTimeout(t *testing.T) { 229 | h := newTestHandler() 230 | l := slog.NewWithHandlers(h) 231 | 232 | // test flush error 233 | h.errOnFlush = true 234 | l.FlushTimeout(time.Millisecond * 2) 235 | 236 | // test flush timeout 237 | h.errOnFlush = false 238 | h.callOnFlush = func() { 239 | time.Sleep(time.Millisecond * 25) 240 | } 241 | l.FlushTimeout(time.Millisecond * 20) 242 | 243 | assert.Panics(t, func() { 244 | l.StopDaemon() 245 | }) 246 | } 247 | 248 | func TestLogger_rewrite_record(t *testing.T) { 249 | h := newTestHandler() 250 | l := slog.NewWithHandlers(h) 251 | 252 | t.Run("Record rewrite", func(t *testing.T) { 253 | r := l.Record() 254 | r.Info("a message1") 255 | fmt.Printf("%+v\n", r) 256 | 257 | time.Sleep(time.Millisecond * 2) 258 | r.Warn("a message2") 259 | fmt.Printf("%+v\n", r) 260 | 261 | time.Sleep(time.Millisecond * 2) 262 | r.Warn("a message3") 263 | fmt.Printf("%+v\n", r) 264 | 265 | r.Release() 266 | dump.P(h.ResetGet()) 267 | }) 268 | 269 | t.Run("Reused rewrite", func(t *testing.T) { 270 | r := l.Reused() 271 | r.Info("A message1") 272 | fmt.Printf("%+v\n", r) 273 | 274 | time.Sleep(time.Millisecond * 2) 275 | r.Warn("A message2") 276 | fmt.Printf("%+v\n", r) 277 | 278 | r.Release() 279 | dump.P(h.ResetGet()) 280 | }) 281 | } 282 | -------------------------------------------------------------------------------- /logger_write.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | // 4 | // --------------------------------------------------------------------------- 5 | // Do write log message 6 | // --------------------------------------------------------------------------- 7 | // 8 | 9 | // func (r *Record) logWrite(level Level) { 10 | // Will reduce memory allocation once 11 | // r.Message = strutil.Byte2str(message) 12 | 13 | // var buf *bytes.Buffer 14 | // buf = bufferPool.Get().(*bytes.Buffer) 15 | // defer bufferPool.Put(buf) 16 | // r.Buffer = buf 17 | 18 | // TODO release on here ?? 19 | // defer r.logger.releaseRecord(r) 20 | // r.logger.writeRecord(level, r) 21 | // r.Buffer = nil 22 | // } 23 | 24 | // Init something for record(eg: time, level name). 25 | func (r *Record) Init(lowerLevelName bool) { 26 | r.inited = true 27 | 28 | // use lower level name 29 | if lowerLevelName { 30 | r.levelName = r.Level.LowerName() 31 | } else { 32 | r.levelName = r.Level.Name() 33 | } 34 | 35 | // init log time 36 | if r.Time.IsZero() { 37 | r.Time = r.logger.TimeClock.Now() 38 | } 39 | 40 | // r.microSecond = r.Time.Nanosecond() / 1000 41 | } 42 | 43 | // Init something for record. 44 | func (r *Record) beforeHandle(l *Logger) { 45 | // log caller. will alloc 3 times 46 | if l.ReportCaller { 47 | caller, ok := getCaller(r.CallerSkip) 48 | if ok { 49 | r.Caller = &caller 50 | } 51 | } 52 | 53 | // processing log record 54 | for i := range l.processors { 55 | l.processors[i].Process(r) 56 | } 57 | } 58 | 59 | // do write record to handlers, will add lock. 60 | func (l *Logger) writeRecord(level Level, r *Record) { 61 | l.mu.Lock() 62 | defer l.mu.Unlock() 63 | // reset init flag, useful for repeat use Record 64 | r.inited = false 65 | 66 | for _, handler := range l.handlers { 67 | if handler.IsHandling(level) { 68 | // init record, call processors 69 | if !r.inited { 70 | r.Init(l.LowerLevelName) 71 | r.beforeHandle(l) 72 | } 73 | 74 | // do write a log message by handler 75 | if err := handler.Handle(r); err != nil { 76 | l.err = err 77 | printlnStderr("slog: failed to handle log, error:", err) 78 | } 79 | } 80 | } 81 | 82 | // ---- after write log ---- 83 | r.Time = emptyTime 84 | 85 | // flush logs on level <= error level. 86 | if level <= ErrorLevel { 87 | l.flushAll() // has been in lock 88 | } 89 | 90 | if level <= PanicLevel { 91 | l.PanicFunc(r) 92 | } else if level <= FatalLevel { 93 | l.Exit(1) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /processor.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "os" 7 | "runtime" 8 | 9 | "github.com/gookit/goutil/strutil" 10 | ) 11 | 12 | // 13 | // Processor interface 14 | // 15 | 16 | // Processor interface definition 17 | type Processor interface { 18 | // Process record 19 | Process(record *Record) 20 | } 21 | 22 | // ProcessorFunc wrapper definition 23 | type ProcessorFunc func(record *Record) 24 | 25 | // Process record 26 | func (fn ProcessorFunc) Process(record *Record) { 27 | fn(record) 28 | } 29 | 30 | // ProcessableHandler interface 31 | type ProcessableHandler interface { 32 | // AddProcessor add a processor 33 | AddProcessor(Processor) 34 | // ProcessRecord handle a record 35 | ProcessRecord(record *Record) 36 | } 37 | 38 | // Processable definition 39 | type Processable struct { 40 | processors []Processor 41 | } 42 | 43 | // AddProcessor to the handler 44 | func (p *Processable) AddProcessor(processor Processor) { 45 | p.processors = append(p.processors, processor) 46 | } 47 | 48 | // ProcessRecord process record 49 | func (p *Processable) ProcessRecord(r *Record) { 50 | // processing log record 51 | for _, processor := range p.processors { 52 | processor.Process(r) 53 | } 54 | } 55 | 56 | // 57 | // there are some built-in processors 58 | // 59 | 60 | // AddHostname to record 61 | func AddHostname() Processor { 62 | hostname, _ := os.Hostname() 63 | return ProcessorFunc(func(record *Record) { 64 | record.AddField("hostname", hostname) 65 | }) 66 | } 67 | 68 | // AddUniqueID to record 69 | func AddUniqueID(fieldName string) Processor { 70 | hs := md5.New() 71 | 72 | return ProcessorFunc(func(record *Record) { 73 | rb, _ := strutil.RandomBytes(32) 74 | hs.Write(rb) 75 | randomID := hex.EncodeToString(hs.Sum(nil)) 76 | hs.Reset() 77 | 78 | record.AddField(fieldName, randomID) 79 | }) 80 | } 81 | 82 | // MemoryUsage get memory usage. 83 | var MemoryUsage ProcessorFunc = func(record *Record) { 84 | stat := new(runtime.MemStats) 85 | runtime.ReadMemStats(stat) 86 | record.SetExtraValue("memoryUsage", stat.Alloc) 87 | } 88 | 89 | // AppendCtxKeys append context keys to record.Fields 90 | func AppendCtxKeys(keys ...string) Processor { 91 | return ProcessorFunc(func(record *Record) { 92 | if record.Ctx == nil { 93 | return 94 | } 95 | 96 | for _, key := range keys { 97 | if val := record.Ctx.Value(key); val != nil { 98 | record.AddField(key, record.Ctx.Value(key)) 99 | } 100 | } 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /processor_test.go: -------------------------------------------------------------------------------- 1 | package slog_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/gookit/goutil/byteutil" 10 | "github.com/gookit/goutil/testutil/assert" 11 | "github.com/gookit/slog" 12 | ) 13 | 14 | func TestLogger_AddProcessor(t *testing.T) { 15 | buf := new(byteutil.Buffer) 16 | 17 | l := slog.NewJSONSugared(buf, slog.InfoLevel) 18 | l.AddProcessor(slog.AddHostname()) 19 | l.Info("message") 20 | 21 | hostname, _ := os.Hostname() 22 | 23 | // {"channel":"application","data":{},"datetime":"2020/07/17 12:01:35","extra":{},"hostname":"InhereMac","level":"INFO","message":"message"} 24 | str := buf.String() 25 | buf.Reset() 26 | assert.Contains(t, str, `"level":"INFO"`) 27 | assert.Contains(t, str, `"message":"message"`) 28 | assert.Contains(t, str, fmt.Sprintf(`"hostname":"%s"`, hostname)) 29 | 30 | l.ResetProcessors() 31 | l.PushProcessor(slog.MemoryUsage) 32 | l.Info("message2") 33 | 34 | // {"channel":"application","data":{},"datetime":"2020/07/16 16:40:18","extra":{"memoryUsage":326072},"level":"INFO","message":"message2"} 35 | str = buf.String() 36 | buf.Reset() 37 | assert.Contains(t, str, `"message":"message2"`) 38 | assert.Contains(t, str, `"memoryUsage":`) 39 | 40 | l.ResetProcessors() 41 | l.SetProcessors([]slog.Processor{slog.AddUniqueID("requestId")}) 42 | l.Info("message3") 43 | str = buf.String() 44 | buf.Reset() 45 | assert.Contains(t, str, `"message":"message3"`) 46 | assert.Contains(t, str, `"requestId":`) 47 | fmt.Print(str) 48 | 49 | l.ResetProcessors() 50 | l.AddProcessors(slog.AppendCtxKeys("traceId", "userId")) 51 | l.Info("message4") 52 | str = buf.ResetAndGet() 53 | fmt.Print(str) 54 | assert.Contains(t, str, `"message":"message4"`) 55 | assert.NotContains(t, str, `"traceId"`) 56 | 57 | ctx := context.WithValue(context.Background(), "traceId", "traceId123abc456") 58 | l.WithCtx(ctx).Info("message5") 59 | str = buf.ResetAndGet() 60 | fmt.Print(str) 61 | assert.Contains(t, str, `"message":"message5"`) 62 | assert.Contains(t, str, `"traceId":"traceId123abc456"`) 63 | } 64 | 65 | func TestProcessable_AddProcessor(t *testing.T) { 66 | ps := &slog.Processable{} 67 | ps.AddProcessor(slog.MemoryUsage) 68 | 69 | r := newLogRecord("error message") 70 | ps.ProcessRecord(r) 71 | 72 | assert.NotEmpty(t, r.Extra) 73 | assert.Contains(t, r.Extra, "memoryUsage") 74 | } 75 | -------------------------------------------------------------------------------- /rotatefile/README.md: -------------------------------------------------------------------------------- 1 | # Rotate File 2 | 3 | `rotatefile` provides simple file rotation, compression and cleanup. 4 | 5 | ## Features 6 | 7 | - Rotate file by size and time 8 | - Custom filename for rotate file by size 9 | - Custom time clock for rotate 10 | - Custom file perm for create log file 11 | - Custom rotate mode: create, rename 12 | - Compress rotated file 13 | - Cleanup old files 14 | 15 | ## Install 16 | 17 | ```bash 18 | go get github.com/gookit/slog/rotatefile 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### Create a file writer 24 | 25 | ```go 26 | logFile := "testdata/go_logger.log" 27 | writer, err := rotatefile.NewConfig(logFile).Create() 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // use writer 33 | writer.Write([]byte("log message\n")) 34 | ``` 35 | 36 | ### Use on another logger 37 | 38 | ```go 39 | package main 40 | 41 | import ( 42 | "log" 43 | 44 | "github.com/gookit/slog/rotatefile" 45 | ) 46 | 47 | func main() { 48 | logFile := "testdata/go_logger.log" 49 | writer, err := rotatefile.NewConfig(logFile).Create() 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | log.SetOutput(writer) 55 | log.Println("log message") 56 | } 57 | ``` 58 | 59 | ### Available config options 60 | 61 | ```go 62 | // Config struct for rotate dispatcher 63 | type Config struct { 64 | // Filepath the log file path, will be rotating 65 | Filepath string `json:"filepath" yaml:"filepath"` 66 | 67 | // FilePerm for create log file. default DefaultFilePerm 68 | FilePerm os.FileMode `json:"file_perm" yaml:"file_perm"` 69 | 70 | // MaxSize file contents max size, unit is bytes. 71 | // If is equals zero, disable rotate file by size 72 | // 73 | // default see DefaultMaxSize 74 | MaxSize uint64 `json:"max_size" yaml:"max_size"` 75 | 76 | // RotateTime the file rotate interval time, unit is seconds. 77 | // If is equals zero, disable rotate file by time 78 | // 79 | // default see EveryHour 80 | RotateTime RotateTime `json:"rotate_time" yaml:"rotate_time"` 81 | 82 | // CloseLock use sync lock on write contents, rotating file. 83 | // 84 | // default: false 85 | CloseLock bool `json:"close_lock" yaml:"close_lock"` 86 | 87 | // BackupNum max number for keep old files. 88 | // 89 | // 0 is not limit, default is DefaultBackNum 90 | BackupNum uint `json:"backup_num" yaml:"backup_num"` 91 | 92 | // BackupTime max time for keep old files, unit is hours. 93 | // 94 | // 0 is not limit, default is DefaultBackTime 95 | BackupTime uint `json:"backup_time" yaml:"backup_time"` 96 | 97 | // Compress determines if the rotated log files should be compressed using gzip. 98 | // The default is not to perform compression. 99 | Compress bool `json:"compress" yaml:"compress"` 100 | 101 | // RenameFunc you can custom-build filename for rotate file by size. 102 | // 103 | // default see DefaultFilenameFn 104 | RenameFunc func(filePath string, rotateNum uint) string 105 | 106 | // TimeClock for rotate 107 | TimeClock Clocker 108 | } 109 | ``` 110 | 111 | ## Files clear 112 | 113 | ```go 114 | fc := rotatefile.NewFilesClear(func(c *rotatefile.CConfig) { 115 | c.AddPattern("/path/to/some*.log") 116 | c.BackupNum = 2 117 | c.BackupTime = 12 // 12 hours 118 | }) 119 | 120 | // clear files on daemon 121 | go fc.DaemonClean(nil) 122 | 123 | // NOTE: stop daemon before exit 124 | // fc.QuitDaemon() 125 | ``` 126 | 127 | ### Configs 128 | 129 | ```go 130 | 131 | // CConfig struct for clean files 132 | type CConfig struct { 133 | // BackupNum max number for keep old files. 134 | // 0 is not limit, default is 20. 135 | BackupNum uint `json:"backup_num" yaml:"backup_num"` 136 | 137 | // BackupTime max time for keep old files, unit is TimeUnit. 138 | // 139 | // 0 is not limit, default is a week. 140 | BackupTime uint `json:"backup_time" yaml:"backup_time"` 141 | 142 | // Compress determines if the rotated log files should be compressed using gzip. 143 | // The default is not to perform compression. 144 | Compress bool `json:"compress" yaml:"compress"` 145 | 146 | // Patterns dir path with filename match patterns. 147 | // 148 | // eg: ["/tmp/error.log.*", "/path/to/info.log.*", "/path/to/dir/*"] 149 | Patterns []string `json:"patterns" yaml:"patterns"` 150 | 151 | // TimeClock for clean files 152 | TimeClock Clocker 153 | 154 | // TimeUnit for BackupTime. default is hours: time.Hour 155 | TimeUnit time.Duration `json:"time_unit" yaml:"time_unit"` 156 | 157 | // CheckInterval for clean files on daemon run. default is 60s. 158 | CheckInterval time.Duration `json:"check_interval" yaml:"check_interval"` 159 | 160 | // IgnoreError ignore remove error 161 | // TODO IgnoreError bool 162 | 163 | // RotateMode for rotate split files TODO 164 | // - copy+cut: copy contents then truncate file 165 | // - rename : rename file(use for like PHP-FPM app) 166 | // RotateMode RotateMode `json:"rotate_mode" yaml:"rotate_mode"` 167 | } 168 | ``` -------------------------------------------------------------------------------- /rotatefile/cleanup.go: -------------------------------------------------------------------------------- 1 | package rotatefile 2 | 3 | import ( 4 | "os" 5 | "sort" 6 | "time" 7 | 8 | "github.com/gookit/goutil/errorx" 9 | "github.com/gookit/goutil/fsutil" 10 | ) 11 | 12 | const defaultCheckInterval = 60 * time.Second 13 | 14 | // CConfig struct for clean files 15 | type CConfig struct { 16 | // BackupNum max number for keep old files. 17 | // 18 | // 0 is not limit, default is 20. 19 | BackupNum uint `json:"backup_num" yaml:"backup_num"` 20 | 21 | // BackupTime max time for keep old files, unit is TimeUnit. 22 | // 23 | // 0 is not limit, default is a week. 24 | BackupTime uint `json:"backup_time" yaml:"backup_time"` 25 | 26 | // Compress determines if the rotated log files should be compressed using gzip. 27 | // The default is not to perform compression. 28 | Compress bool `json:"compress" yaml:"compress"` 29 | 30 | // Patterns dir path with filename match patterns. 31 | // 32 | // eg: ["/tmp/error.log.*", "/path/to/info.log.*", "/path/to/dir/*"] 33 | Patterns []string `json:"patterns" yaml:"patterns"` 34 | 35 | // TimeClock for clean files 36 | TimeClock Clocker 37 | 38 | // TimeUnit for BackupTime. default is hours: time.Hour 39 | TimeUnit time.Duration `json:"time_unit" yaml:"time_unit"` 40 | 41 | // CheckInterval for clean files on daemon run. default is 60s. 42 | CheckInterval time.Duration `json:"check_interval" yaml:"check_interval"` 43 | 44 | // IgnoreError ignore remove error 45 | // TODO IgnoreError bool 46 | 47 | // RotateMode for rotate split files TODO 48 | // - copy+cut: copy contents then truncate file 49 | // - rename : rename file(use for like PHP-FPM app) 50 | // RotateMode RotateMode `json:"rotate_mode" yaml:"rotate_mode"` 51 | } 52 | 53 | // CConfigFunc for clean config 54 | type CConfigFunc func(c *CConfig) 55 | 56 | // AddDirPath for clean, will auto append * for match all files 57 | func (c *CConfig) AddDirPath(dirPaths ...string) *CConfig { 58 | for _, dirPath := range dirPaths { 59 | if !fsutil.IsDir(dirPath) { 60 | continue 61 | } 62 | c.Patterns = append(c.Patterns, dirPath+"/*") 63 | } 64 | return c 65 | } 66 | 67 | // AddPattern for clean. eg: "/tmp/error.log.*" 68 | func (c *CConfig) AddPattern(patterns ...string) *CConfig { 69 | c.Patterns = append(c.Patterns, patterns...) 70 | return c 71 | } 72 | 73 | // WithConfigFn for custom settings 74 | func (c *CConfig) WithConfigFn(fns ...CConfigFunc) *CConfig { 75 | for _, fn := range fns { 76 | if fn != nil { 77 | fn(c) 78 | } 79 | } 80 | return c 81 | } 82 | 83 | // NewCConfig instance 84 | func NewCConfig() *CConfig { 85 | return &CConfig{ 86 | BackupNum: DefaultBackNum, 87 | BackupTime: DefaultBackTime, 88 | TimeClock: DefaultTimeClockFn, 89 | TimeUnit: time.Hour, 90 | // check interval time 91 | CheckInterval: defaultCheckInterval, 92 | } 93 | } 94 | 95 | // FilesClear multi files by time. 96 | // 97 | // use for rotate and clear other program produce log files 98 | type FilesClear struct { 99 | // mu sync.Mutex 100 | cfg *CConfig 101 | // inited mark 102 | inited bool 103 | 104 | // file max backup time. equals CConfig.BackupTime * CConfig.TimeUnit 105 | backupDur time.Duration 106 | quitDaemon chan struct{} 107 | } 108 | 109 | // NewFilesClear instance 110 | func NewFilesClear(fns ...CConfigFunc) *FilesClear { 111 | cfg := NewCConfig().WithConfigFn(fns...) 112 | return &FilesClear{cfg: cfg} 113 | } 114 | 115 | // Config get 116 | func (r *FilesClear) Config() *CConfig { 117 | return r.cfg 118 | } 119 | 120 | // WithConfig for custom set config 121 | func (r *FilesClear) WithConfig(cfg *CConfig) *FilesClear { 122 | r.cfg = cfg 123 | return r 124 | } 125 | 126 | // WithConfigFn for custom settings 127 | func (r *FilesClear) WithConfigFn(fns ...CConfigFunc) *FilesClear { 128 | r.cfg.WithConfigFn(fns...) 129 | return r 130 | } 131 | 132 | // 133 | // --------------------------------------------------------------------------- 134 | // clean backup files 135 | // --------------------------------------------------------------------------- 136 | // 137 | 138 | // StopDaemon for stop daemon clean 139 | func (r *FilesClear) StopDaemon() { 140 | if r.quitDaemon == nil { 141 | panic("cannot quit daemon, please call DaemonClean() first") 142 | } 143 | close(r.quitDaemon) 144 | } 145 | 146 | // DaemonClean daemon clean old files by config 147 | // 148 | // NOTE: this method will block current goroutine 149 | // 150 | // Usage: 151 | // 152 | // fc := rotatefile.NewFilesClear(nil) 153 | // fc.WithConfigFn(func(c *rotatefile.CConfig) { 154 | // c.AddDirPath("./testdata") 155 | // }) 156 | // 157 | // wg := sync.WaitGroup{} 158 | // wg.Add(1) 159 | // 160 | // // start daemon 161 | // go fc.DaemonClean(func() { 162 | // wg.Done() 163 | // }) 164 | // 165 | // // wait for stop 166 | // wg.Wait() 167 | func (r *FilesClear) DaemonClean(onStop func()) { 168 | if r.cfg.BackupNum == 0 && r.cfg.BackupTime == 0 { 169 | panic("clean: backupNum and backupTime are both 0") 170 | } 171 | 172 | r.quitDaemon = make(chan struct{}) 173 | tk := time.NewTicker(r.cfg.CheckInterval) 174 | defer tk.Stop() 175 | 176 | for { 177 | select { 178 | case <-r.quitDaemon: 179 | if onStop != nil { 180 | onStop() 181 | } 182 | return 183 | case <-tk.C: // do cleaning 184 | printErrln("files-clear: cleanup old files error:", r.Clean()) 185 | } 186 | } 187 | } 188 | 189 | // Clean old files by config 190 | func (r *FilesClear) prepare() { 191 | if r.inited { 192 | return 193 | } 194 | r.inited = true 195 | 196 | // check backup time 197 | if r.cfg.BackupTime > 0 { 198 | r.backupDur = time.Duration(r.cfg.BackupTime) * r.cfg.TimeUnit 199 | } 200 | } 201 | 202 | // Clean old files by config 203 | func (r *FilesClear) Clean() error { 204 | if r.cfg.BackupNum == 0 && r.cfg.BackupTime == 0 { 205 | return errorx.Err("clean: backupNum and backupTime are both 0") 206 | } 207 | 208 | // clear by time, can also clean by number 209 | for _, filePattern := range r.cfg.Patterns { 210 | if err := r.cleanByPattern(filePattern); err != nil { 211 | return err 212 | } 213 | } 214 | return nil 215 | } 216 | 217 | // CleanByPattern clean files by pattern 218 | func (r *FilesClear) cleanByPattern(filePattern string) (err error) { 219 | r.prepare() 220 | 221 | oldFiles := make([]fileInfo, 0, 8) 222 | cutTime := r.cfg.TimeClock.Now().Add(-r.backupDur) 223 | 224 | // find and clean expired files 225 | err = fsutil.GlobWithFunc(filePattern, func(filePath string) error { 226 | stat, err := os.Stat(filePath) 227 | if err != nil { 228 | return err 229 | } 230 | 231 | // not handle subdir TODO: support subdir 232 | if stat.IsDir() { 233 | return nil 234 | } 235 | 236 | // collect not expired 237 | if stat.ModTime().After(cutTime) { 238 | oldFiles = append(oldFiles, newFileInfo(filePath, stat)) 239 | return nil 240 | } 241 | 242 | // remove expired file 243 | return r.remove(filePath) 244 | }) 245 | 246 | // clear by backup number. 247 | backNum := int(r.cfg.BackupNum) 248 | remNum := len(oldFiles) - backNum 249 | 250 | if backNum > 0 && remNum > 0 { 251 | // sort by mod-time, oldest at first. 252 | sort.Sort(modTimeFInfos(oldFiles)) 253 | 254 | for idx := 0; idx < len(oldFiles); idx++ { 255 | if err = r.remove(oldFiles[idx].Path()); err != nil { 256 | break 257 | } 258 | 259 | remNum-- 260 | if remNum == 0 { 261 | break 262 | } 263 | } 264 | } 265 | return 266 | } 267 | 268 | func (r *FilesClear) remove(filePath string) (err error) { 269 | return os.Remove(filePath) 270 | } 271 | -------------------------------------------------------------------------------- /rotatefile/cleanup_test.go: -------------------------------------------------------------------------------- 1 | package rotatefile_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gookit/goutil" 11 | "github.com/gookit/goutil/dump" 12 | "github.com/gookit/goutil/fsutil" 13 | "github.com/gookit/goutil/testutil/assert" 14 | "github.com/gookit/goutil/timex" 15 | "github.com/gookit/slog/rotatefile" 16 | ) 17 | 18 | func TestFilesClear_Clean(t *testing.T) { 19 | // make files for clean 20 | makeNum := 5 21 | makeWaitCleanFiles("file_clean.log", makeNum) 22 | _, err := fsutil.PutContents("testdata/subdir/some.txt", "test data") 23 | assert.NoErr(t, err) 24 | 25 | // create clear 26 | fc := rotatefile.NewFilesClear() 27 | fc.WithConfig(rotatefile.NewCConfig()) 28 | fc.WithConfigFn(func(c *rotatefile.CConfig) { 29 | c.AddDirPath("testdata", "not-exist-dir") 30 | c.BackupNum = 1 31 | c.BackupTime = 3 32 | c.TimeUnit = time.Second // for test 33 | }) 34 | 35 | cfg := fc.Config() 36 | assert.Eq(t, uint(1), cfg.BackupNum) 37 | dump.P(cfg) 38 | 39 | // do clean 40 | assert.NoErr(t, fc.Clean()) 41 | 42 | files := fsutil.Glob("testdata/file_clean.log.*") 43 | dump.P(files) 44 | assert.NotEmpty(t, files) 45 | assert.Lt(t, len(files), makeNum) 46 | 47 | t.Run("error", func(t *testing.T) { 48 | fc := rotatefile.NewFilesClear(func(c *rotatefile.CConfig) { 49 | c.BackupNum = 0 50 | c.BackupTime = 0 51 | }) 52 | assert.Err(t, fc.Clean()) 53 | }) 54 | } 55 | 56 | func TestFilesClear_DaemonClean(t *testing.T) { 57 | t.Run("panic", func(t *testing.T) { 58 | fc := rotatefile.NewFilesClear(func(c *rotatefile.CConfig) { 59 | c.BackupNum = 0 60 | c.BackupTime = 0 61 | }) 62 | assert.Panics(t, func() { 63 | fc.StopDaemon() 64 | }) 65 | assert.Panics(t, func() { 66 | fc.DaemonClean(nil) 67 | }) 68 | }) 69 | 70 | fc := rotatefile.NewFilesClear(func(c *rotatefile.CConfig) { 71 | c.AddPattern("testdata/file_daemon_clean.*") 72 | c.BackupNum = 1 73 | c.BackupTime = 3 74 | c.TimeUnit = time.Second // for test 75 | c.CheckInterval = time.Second // for test 76 | }) 77 | 78 | cfg := fc.Config() 79 | dump.P(cfg) 80 | 81 | // make files for clean 82 | makeNum := 5 83 | makeWaitCleanFiles("file_daemon_clean.log", makeNum) 84 | 85 | // test daemon clean 86 | wg := sync.WaitGroup{} 87 | wg.Add(1) 88 | 89 | // start daemon 90 | go fc.DaemonClean(func() { 91 | fmt.Println("daemon clean stopped, at", timex.Now().DateFormat("ymdTH:i:s.v")) 92 | wg.Done() 93 | }) 94 | 95 | // stop daemon 96 | go func() { 97 | time.Sleep(time.Millisecond * 1200) 98 | fmt.Println("stop daemon clean, at", timex.Now().DateFormat("ymdTH:i:s.v")) 99 | fc.StopDaemon() 100 | }() 101 | 102 | // wait for stop 103 | wg.Wait() 104 | 105 | files := fsutil.Glob("testdata/file_daemon_clean.log.*") 106 | dump.P(files) 107 | assert.NotEmpty(t, files) 108 | assert.Lt(t, len(files), makeNum) 109 | } 110 | 111 | func makeWaitCleanFiles(nameTpl string, makeNum int) { 112 | for i := 0; i < makeNum; i++ { 113 | fpath := fmt.Sprintf("testdata/%s.%03d", nameTpl, i) 114 | fmt.Println("make file:", fpath) 115 | _, err := fsutil.PutContents(fpath, []byte("test contents ...")) 116 | goutil.PanicErr(err) 117 | time.Sleep(time.Second) 118 | } 119 | 120 | fmt.Println("wait clean files:") 121 | err := fsutil.GlobWithFunc("./testdata/"+nameTpl+".*", func(fpath string) error { 122 | fi, err := os.Stat(fpath) 123 | goutil.PanicErr(err) 124 | 125 | fmt.Printf(" %s => mtime: %s\n", fpath, fi.ModTime().Format("060102T15:04:05")) 126 | return nil 127 | }) 128 | goutil.PanicErr(err) 129 | } 130 | -------------------------------------------------------------------------------- /rotatefile/config_test.go: -------------------------------------------------------------------------------- 1 | package rotatefile_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/gookit/goutil/dump" 8 | "github.com/gookit/goutil/fmtutil" 9 | "github.com/gookit/goutil/testutil/assert" 10 | "github.com/gookit/goutil/timex" 11 | "github.com/gookit/slog/rotatefile" 12 | ) 13 | 14 | func TestNewDefaultConfig(t *testing.T) { 15 | size := fmtutil.DataSize(1024 * 1024 * 10) 16 | dump.P(size) 17 | 18 | c := rotatefile.NewDefaultConfig() 19 | assert.Eq(t, rotatefile.DefaultMaxSize, c.MaxSize) 20 | } 21 | 22 | func TestNewConfig(t *testing.T) { 23 | cfg := rotatefile.NewConfig("testdata/test.log") 24 | 25 | assert.Eq(t, rotatefile.DefaultBackNum, cfg.BackupNum) 26 | assert.Eq(t, rotatefile.DefaultBackTime, cfg.BackupTime) 27 | assert.Eq(t, rotatefile.EveryHour, cfg.RotateTime) 28 | assert.Eq(t, rotatefile.DefaultMaxSize, cfg.MaxSize) 29 | assert.Eq(t, rotatefile.ModeRename, cfg.RotateMode) 30 | 31 | dump.P(cfg) 32 | 33 | cfg = rotatefile.EmptyConfigWith(func(c *rotatefile.Config) { 34 | c.Compress = true 35 | }) 36 | assert.True(t, cfg.Compress) 37 | assert.Eq(t, uint(0), cfg.BackupNum) 38 | assert.Eq(t, uint(0), cfg.BackupTime) 39 | } 40 | 41 | func TestRotateMode_String(t *testing.T) { 42 | assert.Eq(t, "rename", rotatefile.ModeRename.String()) 43 | assert.Eq(t, "create", rotatefile.ModeCreate.String()) 44 | assert.Eq(t, "unknown", rotatefile.RotateMode(9).String()) 45 | } 46 | 47 | func TestRotateTime_TimeFormat(t *testing.T) { 48 | now := timex.Now() 49 | 50 | rt := rotatefile.EveryDay 51 | assert.Eq(t, "20060102", rt.TimeFormat()) 52 | ft := rt.FirstCheckTime(now.T()) 53 | assert.True(t, now.DayEnd().Equal(ft)) 54 | 55 | rt = rotatefile.EveryHour 56 | assert.Eq(t, "20060102_1500", rt.TimeFormat()) 57 | 58 | rt = rotatefile.Every15Min 59 | assert.Eq(t, "20060102_1504", rt.TimeFormat()) 60 | ft = rt.FirstCheckTime(now.T()) 61 | assert.Gt(t, ft.Unix(), 0) 62 | 63 | rt = rotatefile.EverySecond 64 | assert.Eq(t, "20060102_150405", rt.TimeFormat()) 65 | ft = rt.FirstCheckTime(now.T()) 66 | assert.Eq(t, now.Unix()+rt.Interval(), ft.Unix()) 67 | } 68 | 69 | func TestRotateTime_String(t *testing.T) { 70 | assert.Eq(t, "Every 1 Day", rotatefile.EveryDay.String()) 71 | assert.Eq(t, "Every 1 Hours", rotatefile.EveryHour.String()) 72 | assert.Eq(t, "Every 1 Minutes", rotatefile.EveryMinute.String()) 73 | assert.Eq(t, "Every 1 Seconds", rotatefile.EverySecond.String()) 74 | 75 | assert.Eq(t, "Every 2 Hours", rotatefile.RotateTime(timex.OneHourSec*2).String()) 76 | assert.Eq(t, "Every 15 Minutes", rotatefile.RotateTime(timex.OneMinSec*15).String()) 77 | assert.Eq(t, "Every 5 Minutes", rotatefile.RotateTime(timex.OneMinSec*5).String()) 78 | assert.Eq(t, "Every 3 Seconds", rotatefile.RotateTime(3).String()) 79 | assert.Eq(t, "Every 2 Day", rotatefile.RotateTime(timex.OneDaySec*2).String()) 80 | } 81 | 82 | func TestRotateTime_FirstCheckTime_Round(t *testing.T) { 83 | // log rotate interval minutes 84 | logMin := 5 85 | 86 | // now := timex.Now() 87 | // nowMin := now.Minute() 88 | nowMin := 37 89 | // dur := time.Duration(now.Minute() + min) 90 | dur := time.Duration(nowMin + logMin) 91 | assert.Eq(t, time.Duration(40), dur.Round(time.Duration(logMin))) 92 | 93 | nowMin = 40 94 | dur = time.Duration(nowMin + logMin) 95 | assert.Eq(t, time.Duration(45), dur.Round(time.Duration(logMin))) 96 | 97 | nowMin = 41 98 | dur = time.Duration(nowMin + logMin) 99 | assert.Eq(t, time.Duration(45), dur.Round(time.Duration(logMin))) 100 | } 101 | -------------------------------------------------------------------------------- /rotatefile/issues_test.go: -------------------------------------------------------------------------------- 1 | package rotatefile_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/gookit/goutil/fsutil" 8 | "github.com/gookit/goutil/mathutil" 9 | "github.com/gookit/goutil/testutil/assert" 10 | "github.com/gookit/slog/internal" 11 | "github.com/gookit/slog/rotatefile" 12 | ) 13 | 14 | // https://github.com/gookit/slog/issues/138 15 | // 日志按everyday自动滚动,文件名的日期对应的是前一天的日志 #138 16 | func TestIssues_138(t *testing.T) { 17 | logfile := "testdata/iss138_rotate_day.log" 18 | 19 | mt := rotatefile.NewMockClock("2023-11-16 23:59:55") 20 | w, err := rotatefile.NewWriterWith(rotatefile.WithDebugMode, func(c *rotatefile.Config) { 21 | c.TimeClock = mt 22 | // c.MaxSize = 128 23 | c.Filepath = logfile 24 | c.RotateTime = rotatefile.EveryDay 25 | }) 26 | 27 | assert.NoErr(t, err) 28 | defer w.MustClose() 29 | 30 | for i := 0; i < 15; i++ { 31 | dt := mt.Datetime() 32 | _, err = w.WriteString(dt + " [INFO] this is a log message, idx=" + mathutil.String(i) + "\n") 33 | assert.NoErr(t, err) 34 | // increase time 35 | mt.Add(time.Second * 3) 36 | // mt.Add(time.Millisecond * 300) 37 | } 38 | 39 | // Out: rotate_day.log, rotate_day.log.20231116 40 | files := fsutil.Glob(internal.BuildGlobPattern(logfile)) 41 | assert.Len(t, files, 2) 42 | 43 | // check contents 44 | assert.True(t, fsutil.IsFile(logfile)) 45 | s := fsutil.ReadString(logfile) 46 | assert.StrContains(t, s, "2023-11-17 00:00") 47 | 48 | oldFile := internal.AddSuffix2path(logfile, "20231116") 49 | assert.True(t, fsutil.IsFile(oldFile)) 50 | s = fsutil.ReadString(oldFile) 51 | assert.StrContains(t, s, "2023-11-16 23:") 52 | } 53 | 54 | // https://github.com/gookit/slog/issues/150 55 | // 日志轮转时间设置为分钟时,FirstCheckTime计算单位错误,导致生成预期外的多个日志文件 #150 56 | func TestIssues_150(t *testing.T) { 57 | logfile := "testdata/iss150_rotate_min.log" 58 | 59 | mt := rotatefile.NewMockClock("2024-09-14 18:39:55") 60 | w, err := rotatefile.NewWriterWith(rotatefile.WithDebugMode, func(c *rotatefile.Config) { 61 | c.TimeClock = mt 62 | // c.MaxSize = 128 63 | c.Filepath = logfile 64 | c.RotateTime = rotatefile.EveryMinute * 3 65 | }) 66 | 67 | assert.NoErr(t, err) 68 | defer w.MustClose() 69 | 70 | for i := 0; i < 15; i++ { 71 | dt := mt.Datetime() 72 | _, err = w.WriteString(dt + " [INFO] this is a log message, idx=" + mathutil.String(i) + "\n") 73 | assert.NoErr(t, err) 74 | // increase time 75 | mt.Add(time.Minute * 1) 76 | } 77 | 78 | files := fsutil.Glob(internal.BuildGlobPattern(logfile)) 79 | assert.LenGt(t, files, 3) 80 | 81 | // check contents 82 | assert.True(t, fsutil.IsFile(logfile)) 83 | s := fsutil.ReadString(logfile) 84 | assert.StrContains(t, s, "2024-09-14 18:") 85 | 86 | // iss150_rotate_min.20240914_1842.log 87 | oldFile := internal.AddSuffix2path(logfile, "20240914_1842") 88 | assert.True(t, fsutil.IsFile(oldFile)) 89 | s = fsutil.ReadString(oldFile) 90 | assert.StrContains(t, s, "2024-09-14 18:41") 91 | } 92 | -------------------------------------------------------------------------------- /rotatefile/rotatefile.go: -------------------------------------------------------------------------------- 1 | // Package rotatefile provides simple file rotation, compression and cleanup. 2 | package rotatefile 3 | 4 | import ( 5 | "io" 6 | ) 7 | 8 | // RotateWriter interface 9 | type RotateWriter interface { 10 | io.WriteCloser 11 | Clean() error 12 | Flush() error 13 | Rotate() error 14 | Sync() error 15 | } 16 | 17 | // RotateMode for rotate file. 0: rename, 1: create 18 | type RotateMode uint8 19 | 20 | // String get string name 21 | func (m RotateMode) String() string { 22 | switch m { 23 | case ModeRename: 24 | return "rename" 25 | case ModeCreate: 26 | return "create" 27 | default: 28 | return "unknown" 29 | } 30 | } 31 | 32 | const ( 33 | // ModeRename rotating file by rename. 34 | // 35 | // Example flow: 36 | // - always write to "error.log" 37 | // - rotating by rename it to "error.log.20201223" 38 | // - then re-create "error.log" 39 | ModeRename RotateMode = iota 40 | 41 | // ModeCreate rotating file by create new file. 42 | // 43 | // Example flow: 44 | // - directly create new file on each rotate time. eg: "error.log.20201223", "error.log.20201224" 45 | ModeCreate 46 | ) 47 | 48 | const ( 49 | // OneMByte size 50 | OneMByte uint64 = 1024 * 1024 51 | 52 | // DefaultMaxSize of a log file. default is 20M. 53 | DefaultMaxSize = 20 * OneMByte 54 | // DefaultBackNum default backup numbers for old files. 55 | DefaultBackNum uint = 20 56 | // DefaultBackTime default backup time for old files. default keep a week. 57 | DefaultBackTime uint = 24 * 7 58 | ) 59 | -------------------------------------------------------------------------------- /rotatefile/rotatefile_test.go: -------------------------------------------------------------------------------- 1 | package rotatefile_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/gookit/goutil" 9 | "github.com/gookit/goutil/fsutil" 10 | "github.com/gookit/slog/rotatefile" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | fmt.Println("TestMain: remove all test files in ./testdata") 15 | goutil.PanicErr(fsutil.RemoveSub("./testdata", fsutil.ExcludeNames(".keep"))) 16 | m.Run() 17 | } 18 | 19 | func ExampleNewWriter_on_other_logger() { 20 | logFile := "testdata/another_logger.log" 21 | writer, err := rotatefile.NewConfig(logFile).Create() 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | log.SetOutput(writer) 27 | log.Println("log message") 28 | } 29 | -------------------------------------------------------------------------------- /rotatefile/util.go: -------------------------------------------------------------------------------- 1 | package rotatefile 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "time" 10 | 11 | "github.com/gookit/goutil" 12 | "github.com/gookit/goutil/fsutil" 13 | "github.com/gookit/goutil/timex" 14 | ) 15 | 16 | const compressSuffix = ".gz" 17 | 18 | func printErrln(pfx string, err error) { 19 | if err != nil { 20 | _, _ = fmt.Fprintln(os.Stderr, pfx, err) 21 | } 22 | } 23 | 24 | func compressFile(srcPath, dstPath string) error { 25 | srcFile, err := os.OpenFile(srcPath, os.O_RDONLY, 0) 26 | if err != nil { 27 | return err 28 | } 29 | defer srcFile.Close() 30 | 31 | // create and open a gz file 32 | gzFile, err := fsutil.OpenTruncFile(dstPath) 33 | if err != nil { 34 | return err 35 | } 36 | defer gzFile.Close() 37 | 38 | srcSt, err := srcFile.Stat() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | zw := gzip.NewWriter(gzFile) 44 | zw.Name = srcSt.Name() 45 | zw.ModTime = srcSt.ModTime() 46 | 47 | // do copy 48 | if _, err = io.Copy(zw, srcFile); err != nil { 49 | _ = zw.Close() 50 | return err 51 | } 52 | return zw.Close() 53 | } 54 | 55 | // TODO replace to fsutil.FileInfo 56 | type fileInfo struct { 57 | fs.FileInfo 58 | filePath string 59 | } 60 | 61 | // Path get file full path. eg: "/path/to/file.go" 62 | func (fi *fileInfo) Path() string { 63 | return fi.filePath 64 | } 65 | 66 | func newFileInfo(filePath string, fi fs.FileInfo) fileInfo { 67 | return fileInfo{filePath: filePath, FileInfo: fi} 68 | } 69 | 70 | // modTimeFInfos sorts by oldest time modified in the fileInfo. 71 | // eg: [old_220211, old_220212, old_220213] 72 | type modTimeFInfos []fileInfo 73 | 74 | // Less check 75 | func (fis modTimeFInfos) Less(i, j int) bool { 76 | return fis[j].ModTime().After(fis[i].ModTime()) 77 | } 78 | 79 | // Swap value 80 | func (fis modTimeFInfos) Swap(i, j int) { 81 | fis[i], fis[j] = fis[j], fis[i] 82 | } 83 | 84 | // Len get 85 | func (fis modTimeFInfos) Len() int { 86 | return len(fis) 87 | } 88 | 89 | // MockClocker mock clock for test 90 | type MockClocker struct { 91 | tt time.Time 92 | } 93 | 94 | // NewMockClock create a mock time instance from datetime string. 95 | func NewMockClock(datetime string) *MockClocker { 96 | nt := goutil.Must(timex.FromString(datetime)) 97 | return &MockClocker{tt: nt.Time} 98 | } 99 | 100 | // Now get current time. 101 | func (mt *MockClocker) Now() time.Time { 102 | return mt.tt 103 | } 104 | 105 | // Add progresses time by the given duration. 106 | func (mt *MockClocker) Add(d time.Duration) { 107 | mt.tt = mt.tt.Add(d) 108 | } 109 | 110 | // Datetime returns the current time in the format "2006-01-02 15:04:05". 111 | func (mt *MockClocker) Datetime() string { 112 | return mt.tt.Format("2006-01-02 15:04:05") 113 | } 114 | -------------------------------------------------------------------------------- /rotatefile/util_test.go: -------------------------------------------------------------------------------- 1 | package rotatefile 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestPrintErrln(t *testing.T) { 9 | printErrln("test", nil) 10 | printErrln("test", errors.New("an error")) 11 | } 12 | -------------------------------------------------------------------------------- /rotatefile/writer_test.go: -------------------------------------------------------------------------------- 1 | package rotatefile_test 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gookit/goutil/dump" 9 | "github.com/gookit/goutil/fsutil" 10 | "github.com/gookit/goutil/mathutil" 11 | "github.com/gookit/goutil/testutil/assert" 12 | "github.com/gookit/slog/internal" 13 | "github.com/gookit/slog/rotatefile" 14 | ) 15 | 16 | func TestNewWriter(t *testing.T) { 17 | testFile := "testdata/test_writer.log" 18 | assert.NoErr(t, fsutil.DeleteIfExist(testFile)) 19 | 20 | w, err := rotatefile.NewConfig(testFile).Create() 21 | assert.NoErr(t, err) 22 | 23 | c := w.Config() 24 | // dump.P(c) 25 | assert.Eq(t, c.MaxSize, rotatefile.DefaultMaxSize) 26 | 27 | _, err = w.WriteString("info log message\n") 28 | assert.NoErr(t, err) 29 | assert.True(t, fsutil.IsFile(testFile)) 30 | 31 | assert.NoErr(t, w.Sync()) 32 | assert.NoErr(t, w.Flush()) 33 | assert.NoErr(t, w.Close()) 34 | 35 | w, err = rotatefile.NewWriterWith(rotatefile.WithFilepath(testFile)) 36 | assert.NoErr(t, err) 37 | assert.Eq(t, w.Config().Filepath, testFile) 38 | } 39 | 40 | func TestWriter_Rotate_modeCreate(t *testing.T) { 41 | logfile := "testdata/mode_create.log" 42 | 43 | c := rotatefile.NewConfig(logfile) 44 | c.RotateMode = rotatefile.ModeCreate 45 | 46 | wr, err := c.Create() 47 | assert.NoErr(t, err) 48 | _, err = wr.WriteString("[INFO] this is a log message\n") 49 | assert.NoErr(t, err) 50 | assert.False(t, fsutil.IsFile(logfile)) 51 | 52 | ls, err := filepath.Glob("testdata/mode_create*") 53 | assert.NoErr(t, err) 54 | assert.Len(t, ls, 1) 55 | 56 | for i := 0; i < 20; i++ { 57 | _, err = wr.WriteString("[INFO] this is a log message, idx=" + mathutil.String(i) + "\n") 58 | assert.NoErr(t, err) 59 | } 60 | 61 | // test clean and backup 62 | c.BackupNum = 2 63 | c.MaxSize = 128 64 | err = wr.Rotate() 65 | assert.NoErr(t, err) 66 | _, err = wr.WriteString("hi, rotated\n") 67 | assert.NoErr(t, err) 68 | } 69 | 70 | func TestWriter_rotateByTime(t *testing.T) { 71 | logfile := "testdata/rotate-by-time.log" 72 | c := rotatefile.NewConfig(logfile).With(func(c *rotatefile.Config) { 73 | c.DebugMode = true 74 | c.Compress = true 75 | c.RotateTime = rotatefile.EverySecond * 2 76 | }) 77 | 78 | w, err := c.Create() 79 | assert.NoErr(t, err) 80 | defer func() { 81 | _ = w.Close() 82 | }() 83 | 84 | for i := 0; i < 5; i++ { 85 | _, err = w.WriteString("[INFO] this is a log message, idx=" + mathutil.String(i) + "\n") 86 | assert.NoErr(t, err) 87 | time.Sleep(time.Second) 88 | } 89 | 90 | files := fsutil.Glob(internal.BuildGlobPattern(logfile)) 91 | dump.P(files) 92 | 93 | } 94 | 95 | func TestWriter_Clean(t *testing.T) { 96 | logfile := "testdata/writer_clean.log" 97 | 98 | c := rotatefile.NewConfig(logfile) 99 | c.MaxSize = 128 // will rotate by size 100 | 101 | wr, err := c.Create() 102 | assert.NoErr(t, err) 103 | defer func() { 104 | _ = wr.Close() 105 | }() 106 | 107 | for i := 0; i < 20; i++ { 108 | _, err = wr.WriteString("[INFO] this is a log message, idx=" + mathutil.String(i) + "\n") 109 | assert.NoErr(t, err) 110 | } 111 | 112 | assert.True(t, fsutil.IsFile(logfile)) 113 | _, err = wr.WriteString("hi\n") 114 | assert.NoErr(t, err) 115 | 116 | files := fsutil.Glob(internal.BuildGlobPattern(logfile)) 117 | dump.P(files) 118 | 119 | // test clean error 120 | t.Run("clean error", func(t *testing.T) { 121 | c.BackupNum = 0 122 | c.BackupTime = 0 123 | assert.Err(t, wr.Clean()) 124 | }) 125 | 126 | // test clean and compress backup 127 | t.Run("clean and compress", func(t *testing.T) { 128 | c.BackupNum = 2 129 | c.Compress = true 130 | err = wr.Clean() 131 | assert.NoErr(t, err) 132 | 133 | files := fsutil.Glob(internal.BuildGlobPattern(logfile)) 134 | assert.Lt(t, 2, len(files)) 135 | }) 136 | } 137 | 138 | // test writer compress 139 | func TestWriter_Compress(t *testing.T) { 140 | logfile := "testdata/test_compress.log" 141 | 142 | c := rotatefile.NewConfig(logfile) 143 | c.MaxSize = 128 // will rotate by size 144 | c.With(rotatefile.WithDebugMode) 145 | 146 | wr, err := c.Create() 147 | assert.NoErr(t, err) 148 | 149 | for i := 0; i < 20; i++ { 150 | _, err = wr.WriteString("[INFO] this is a log message, idx=" + mathutil.String(i) + "\n") 151 | assert.NoErr(t, err) 152 | } 153 | 154 | assert.True(t, fsutil.IsFile(logfile)) 155 | _, err = wr.WriteString("hi\n") 156 | assert.NoErr(t, err) 157 | wr.MustClose() 158 | 159 | files := fsutil.Glob(internal.BuildGlobPattern(logfile)) 160 | assert.NotEmpty(t, files) 161 | dump.P(files) 162 | 163 | // test clean and compress backup 164 | t.Run("compress backup", func(t *testing.T) { 165 | c := rotatefile.NewConfig(logfile, 166 | rotatefile.WithDebugMode, rotatefile.WithCompress, 167 | rotatefile.WithBackupNum(2), 168 | ) 169 | 170 | wr, err := c.Create() 171 | assert.NoErr(t, err) 172 | defer wr.MustClose() 173 | 174 | err = wr.Clean() 175 | assert.NoErr(t, err) 176 | 177 | files := fsutil.Glob(internal.BuildGlobPattern(logfile)) 178 | assert.Lt(t, 2, len(files)) 179 | dump.P(files) 180 | }) 181 | } 182 | 183 | // TODO set github.com/benbjohnson/clock for mock clock 184 | type constantClock time.Time 185 | 186 | func (c constantClock) Now() time.Time { return time.Time(c) } 187 | func (c constantClock) NewTicker(d time.Duration) *time.Ticker { 188 | return &time.Ticker{} 189 | } 190 | -------------------------------------------------------------------------------- /sugared.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/gookit/color" 8 | ) 9 | 10 | // SugaredLoggerFn func type. 11 | type SugaredLoggerFn func(sl *SugaredLogger) 12 | 13 | // SugaredLogger Is a fast and usable Logger, which already contains 14 | // the default formatting and handling capabilities 15 | type SugaredLogger struct { 16 | *Logger 17 | // Formatter log message formatter. default use TextFormatter 18 | Formatter Formatter 19 | // Output writer 20 | Output io.Writer 21 | // Level for log handling. if log record level <= Level, it will be record. 22 | Level Level 23 | } 24 | 25 | // NewStd logger instance, alias of NewStdLogger() 26 | func NewStd(fns ...SugaredLoggerFn) *SugaredLogger { 27 | return NewStdLogger(fns...) 28 | } 29 | 30 | // NewStdLogger instance 31 | func NewStdLogger(fns ...SugaredLoggerFn) *SugaredLogger { 32 | setFns := []SugaredLoggerFn{ 33 | func(sl *SugaredLogger) { 34 | sl.SetName("stdLogger") 35 | // sl.CallerSkip += 1 36 | sl.ReportCaller = true 37 | // auto enable console color 38 | sl.Formatter.(*TextFormatter).EnableColor = color.SupportColor() 39 | }, 40 | } 41 | 42 | if len(fns) > 0 { 43 | setFns = append(setFns, fns...) 44 | } 45 | return NewSugaredLogger(os.Stdout, DebugLevel, setFns...) 46 | } 47 | 48 | // NewSugared create new SugaredLogger. alias of NewSugaredLogger() 49 | func NewSugared(out io.Writer, level Level, fns ...SugaredLoggerFn) *SugaredLogger { 50 | return NewSugaredLogger(out, level, fns...) 51 | } 52 | 53 | // NewSugaredLogger create new SugaredLogger 54 | func NewSugaredLogger(output io.Writer, level Level, fns ...SugaredLoggerFn) *SugaredLogger { 55 | sl := &SugaredLogger{ 56 | Level: level, 57 | Output: output, 58 | Logger: New(), 59 | // default value 60 | Formatter: NewTextFormatter(), 61 | } 62 | 63 | // NOTICE: use self as an log handler 64 | sl.AddHandler(sl) 65 | 66 | return sl.Config(fns...) 67 | } 68 | 69 | // NewJSONSugared create new SugaredLogger with JSONFormatter 70 | func NewJSONSugared(out io.Writer, level Level, fns ...SugaredLoggerFn) *SugaredLogger { 71 | sl := NewSugaredLogger(out, level) 72 | sl.Formatter = NewJSONFormatter() 73 | 74 | return sl.Config(fns...) 75 | } 76 | 77 | // Config current logger 78 | func (sl *SugaredLogger) Config(fns ...SugaredLoggerFn) *SugaredLogger { 79 | for _, fn := range fns { 80 | fn(sl) 81 | } 82 | return sl 83 | } 84 | 85 | // Reset the logger 86 | func (sl *SugaredLogger) Reset() { 87 | *sl = *NewSugaredLogger(os.Stdout, DebugLevel) 88 | } 89 | 90 | // IsHandling Check if the current level can be handling 91 | func (sl *SugaredLogger) IsHandling(level Level) bool { 92 | return sl.Level.ShouldHandling(level) 93 | } 94 | 95 | // Handle log record 96 | func (sl *SugaredLogger) Handle(record *Record) error { 97 | bts, err := sl.Formatter.Format(record) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | _, err = sl.Output.Write(bts) 103 | return err 104 | } 105 | 106 | // Close all log handlers, will flush and close all handlers. 107 | // 108 | // IMPORTANT: 109 | // 110 | // if enable async/buffer mode, please call the Close() before exit. 111 | func (sl *SugaredLogger) Close() error { 112 | _ = sl.Logger.VisitAll(func(handler Handler) error { 113 | // TIP: must exclude self, because self is a handler 114 | if _, ok := handler.(*SugaredLogger); !ok { 115 | if err := handler.Close(); err != nil { 116 | sl.err = err 117 | } 118 | } 119 | return nil 120 | }) 121 | 122 | return sl.err 123 | } 124 | 125 | // Flush all logs. alias of the FlushAll() 126 | func (sl *SugaredLogger) Flush() error { 127 | return sl.FlushAll() 128 | } 129 | 130 | // FlushAll all logs 131 | func (sl *SugaredLogger) FlushAll() error { 132 | return sl.Logger.VisitAll(func(handler Handler) error { 133 | if _, ok := handler.(*SugaredLogger); !ok { 134 | _ = handler.Flush() 135 | } 136 | return nil 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /testdata/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/slog/90c4be86a3060e094e279af7009139357eaf41a5/testdata/.keep -------------------------------------------------------------------------------- /testdata/runtime.Frame.txt: -------------------------------------------------------------------------------- 1 | *runtime.Frame { 2 | PC: 0x73139f, 3 | Func: &runtime.Func{opaque:struct {}{}}, 4 | Function: "github.com/gookit/slog_test.TestLogger_ReportCaller", 5 | File: "F:/work/go/gookit/slog/logger_test.go", 6 | Line: 47, 7 | Entry: 0x7311b0, 8 | funcInfo: "", 9 | } -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/gookit/goutil/byteutil" 12 | "github.com/gookit/goutil/strutil" 13 | "github.com/valyala/bytebufferpool" 14 | ) 15 | 16 | // const ( 17 | // defaultMaxCallerDepth int = 15 18 | // defaultKnownSlogFrames int = 4 19 | // ) 20 | 21 | // Stack that attempts to recover the data for all goroutines. 22 | // func getCallStacks(callerSkip int) []byte { 23 | // return nil 24 | // } 25 | 26 | // FormatLevelName Format the level name, specify the length returned, 27 | // fill the space with less length, and truncate than the length 28 | func FormatLevelName(name string, length int) string { 29 | if len(name) < length { 30 | return fmt.Sprintf("%-"+strconv.Itoa(length)+"s", name) 31 | } 32 | return name[:length] 33 | } 34 | 35 | func buildLowerLevelName() map[Level]string { 36 | mp := make(map[Level]string, len(LevelNames)) 37 | for level, s := range LevelNames { 38 | mp[level] = strings.ToLower(s) 39 | } 40 | return mp 41 | } 42 | 43 | // getCaller retrieves the name of the first non-slog calling function 44 | func getCaller(callerSkip int) (fr runtime.Frame, ok bool) { 45 | pcs := make([]uintptr, 1) // alloc 1 times 46 | num := runtime.Callers(callerSkip, pcs) 47 | if num < 1 { 48 | return 49 | } 50 | 51 | f, _ := runtime.CallersFrames(pcs).Next() 52 | return f, f.PC != 0 53 | } 54 | 55 | func formatCaller(rf *runtime.Frame, flag uint8, userFn CallerFormatFn) (cs string) { 56 | if userFn != nil { 57 | return userFn(rf) 58 | } 59 | 60 | lineNum := strconv.FormatInt(int64(rf.Line), 10) 61 | switch flag { 62 | case CallerFlagFull: 63 | return rf.Function + "," + filepath.Base(rf.File) + ":" + lineNum 64 | case CallerFlagFunc: 65 | return rf.Function 66 | case CallerFlagFcLine: 67 | return rf.Function + ":" + lineNum 68 | case CallerFlagPkg: 69 | i := strings.LastIndex(rf.Function, "/") 70 | i += strings.IndexByte(rf.Function[i+1:], '.') 71 | return rf.Function[:i+1] 72 | case CallerFlagPkgFnl: 73 | i := strings.LastIndex(rf.Function, "/") 74 | i += strings.IndexByte(rf.Function[i+1:], '.') 75 | return rf.Function[:i+1] + "," + filepath.Base(rf.File) + ":" + lineNum 76 | case CallerFlagFnlFcn: 77 | ss := strings.Split(rf.Function, ".") 78 | return filepath.Base(rf.File) + ":" + lineNum + "," + ss[len(ss)-1] 79 | case CallerFlagFnLine: 80 | return filepath.Base(rf.File) + ":" + lineNum 81 | case CallerFlagFcName: 82 | ss := strings.Split(rf.Function, ".") 83 | return ss[len(ss)-1] 84 | default: // CallerFlagFpLine 85 | return rf.File + ":" + lineNum 86 | } 87 | } 88 | 89 | var msgBufPool bytebufferpool.Pool 90 | 91 | // it like Println, will add spaces for each argument 92 | func formatArgsWithSpaces(vs []any) string { 93 | ln := len(vs) 94 | if ln == 0 { 95 | return "" 96 | } 97 | 98 | if ln == 1 { 99 | return strutil.SafeString(vs[0]) // perf: Reduce one memory allocation 100 | } 101 | 102 | // buf = make([]byte, 0, ln*8) 103 | bb := msgBufPool.Get() 104 | defer msgBufPool.Put(bb) 105 | 106 | // TIP: 107 | // `float` to string - will alloc 2 times memory 108 | // `int <0`, `int > 100` to string - will alloc 1 times memory 109 | for i := range vs { 110 | if i > 0 { // add space 111 | bb.B = append(bb.B, ' ') 112 | } 113 | bb.B = byteutil.AppendAny(bb.B, vs[i]) 114 | } 115 | 116 | return string(bb.B) 117 | // return byteutil.String(bb.B) // perf: Reduce one memory allocation 118 | } 119 | 120 | // EncodeToString data to string 121 | func EncodeToString(v any) string { 122 | if mp, ok := v.(map[string]any); ok { 123 | return mapToString(mp) 124 | } 125 | return strutil.SafeString(v) 126 | } 127 | 128 | func mapToString(mp map[string]any) string { 129 | ln := len(mp) 130 | if ln == 0 { 131 | return "{}" 132 | } 133 | 134 | // TODO use bytebufferpool 135 | buf := make([]byte, 0, ln*8) 136 | buf = append(buf, '{') 137 | 138 | for k, val := range mp { 139 | buf = append(buf, k...) 140 | buf = append(buf, ':') 141 | 142 | str, _ := strutil.AnyToString(val, false) 143 | buf = append(buf, str...) 144 | buf = append(buf, ',', ' ') 145 | } 146 | 147 | // remove last ', ' 148 | buf = append(buf[:len(buf)-2], '}') 149 | return strutil.Byte2str(buf) 150 | } 151 | 152 | func parseTemplateToFields(tplStr string) []string { 153 | ss := strings.Split(tplStr, "{{") 154 | 155 | vars := make([]string, 0, len(ss)*2) 156 | for _, s := range ss { 157 | if len(s) == 0 { 158 | continue 159 | } 160 | 161 | fieldAndOther := strings.SplitN(s, "}}", 2) 162 | if len(fieldAndOther) < 2 { 163 | vars = append(vars, s) 164 | } else { 165 | vars = append(vars, fieldAndOther[0], "}}"+fieldAndOther[1]) 166 | } 167 | } 168 | 169 | return vars 170 | } 171 | 172 | func printlnStderr(args ...any) { 173 | _, _ = fmt.Fprintln(os.Stderr, args...) 174 | } 175 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/gookit/goutil/errorx" 8 | "github.com/gookit/goutil/testutil/assert" 9 | "github.com/gookit/goutil/timex" 10 | ) 11 | 12 | func revertTemplateString(ss []string) string { 13 | var sb strings.Builder 14 | for _, s := range ss { 15 | // is field 16 | if s[0] >= 'a' && s[0] <= 'z' { 17 | sb.WriteString("{{") 18 | sb.WriteString(s) 19 | // sb.WriteString("}}") 20 | } else { 21 | sb.WriteString(s) 22 | } 23 | } 24 | 25 | // sb.WriteByte('\n') 26 | return sb.String() 27 | } 28 | 29 | func TestInner_parseTemplateToFields(t *testing.T) { 30 | ss := parseTemplateToFields(NamedTemplate) 31 | str := revertTemplateString(ss) 32 | // dump.P(ss, str) 33 | assert.Eq(t, NamedTemplate, str) 34 | 35 | ss = parseTemplateToFields(DefaultTemplate) 36 | str = revertTemplateString(ss) 37 | // dump.P(ss, str) 38 | assert.Eq(t, DefaultTemplate, str) 39 | 40 | testTemplate := "[{{datetime}}] [{{level}}] {{message}} {{data}} {{extra}}" 41 | ss = parseTemplateToFields(testTemplate) 42 | str = revertTemplateString(ss) 43 | assert.Eq(t, testTemplate, str) 44 | // dump.P(ss, str) 45 | } 46 | 47 | func TestUtil_EncodeToString(t *testing.T) { 48 | assert.Eq(t, "{a:1}", EncodeToString(map[string]any{"a": 1})) 49 | } 50 | 51 | func TestUtil_formatArgsWithSpaces(t *testing.T) { 52 | // tests for formatArgsWithSpaces 53 | tests := []struct { 54 | args []any 55 | want string 56 | }{ 57 | {nil, ""}, 58 | {[]any{"a", "b", "c"}, "a b c"}, 59 | {[]any{"a", "b", "c", 1, 2, 3}, "a b c 1 2 3"}, 60 | {[]any{"a", 1, nil}, "a 1 "}, 61 | {[]any{12, int8(12), int16(12), int32(12), int64(12)}, "12 12 12 12 12"}, 62 | {[]any{uint(12), uint8(12), uint16(12), uint32(12), uint64(12)}, "12 12 12 12 12"}, 63 | {[]any{float32(12.12), 12.12}, "12.12 12.12"}, 64 | {[]any{true, false}, "true false"}, 65 | {[]any{[]byte("abc"), []byte("123")}, "abc 123"}, 66 | {[]any{timex.OneHour}, "3600000000000"}, 67 | {[]any{errorx.Raw("a error message")}, "a error message"}, 68 | {[]any{[]int{1, 2, 3}}, "[1 2 3]"}, 69 | } 70 | 71 | for _, tt := range tests { 72 | assert.Eq(t, tt.want, formatArgsWithSpaces(tt.args)) 73 | } 74 | 75 | assert.NotEmpty(t, formatArgsWithSpaces([]any{timex.Now().T()})) 76 | } 77 | --------------------------------------------------------------------------------