pax_global_header00006660000000000000000000000064145502430760014520gustar00rootroot0000000000000052 comment=5a5b0af7a8eca56f76341aa8ff3406714af6d9b8 console-slog-0.3.1/000077500000000000000000000000001455024307600141255ustar00rootroot00000000000000console-slog-0.3.1/.github/000077500000000000000000000000001455024307600154655ustar00rootroot00000000000000console-slog-0.3.1/.github/dependabot.yml000066400000000000000000000007701455024307600203210ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "gomod" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" console-slog-0.3.1/.github/workflows/000077500000000000000000000000001455024307600175225ustar00rootroot00000000000000console-slog-0.3.1/.github/workflows/go.yml000066400000000000000000000012161455024307600206520ustar00rootroot00000000000000# This workflow will build a golang project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go name: Build on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Build run: go build -v ./... - name: Test run: go test -race -coverprofile=coverage.txt -covermode=atomic -v ./... - name: Upload coverage to Codecov uses: codecov/codecov-action@v3console-slog-0.3.1/.gitignore000066400000000000000000000007361455024307600161230ustar00rootroot00000000000000# If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work console-slog-0.3.1/LICENSE000066400000000000000000000020671455024307600151370ustar00rootroot00000000000000MIT License Copyright (c) 2023 Pierre-Henri Symoneaux Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. console-slog-0.3.1/README.md000066400000000000000000000060251455024307600154070ustar00rootroot00000000000000# console-slog [![Go Reference](https://pkg.go.dev/badge/github.com/phsym/console-slog.svg)](https://pkg.go.dev/github.com/phsym/console-slog) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/phsym/console-slog/master/LICENSE) [![Build](https://github.com/phsym/console-slog/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/phsym/slog-console/actions/workflows/go.yml) [![codecov](https://codecov.io/gh/phsym/console-slog/graph/badge.svg?token=ZIJT9L79QP)](https://codecov.io/gh/phsym/console-slog) [![Go Report Card](https://goreportcard.com/badge/github.com/phsym/console-slog)](https://goreportcard.com/report/github.com/phsym/console-slog) A handler for slog that prints colorized logs, similar to zerolog's console writer output without sacrificing performances. ## Installation ```bash go get github.com/phsym/console-slog@latest ``` ## Example ```go package main import ( "errors" "log/slog" "os" "github.com/phsym/console-slog" ) func main() { logger := slog.New( console.NewHandler(os.Stderr, &console.HandlerOptions{Level: slog.LevelDebug}), ) slog.SetDefault(logger) slog.Info("Hello world!", "foo", "bar") slog.Debug("Debug message") slog.Warn("Warning message") slog.Error("Error message", "err", errors.New("the error")) logger = logger.With("foo", "bar"). WithGroup("the-group"). With("bar", "baz") logger.Info("group info", "attr", "value") } ``` ![output](./doc/img/output.png) When setting `console.HandlerOptions.AddSource` to `true`: ```go console.NewHandler(os.Stderr, &console.HandlerOptions{Level: slog.LevelDebug, AddSource: true}) ``` ![output-with-source](./doc/img/output-with-source.png) ## Performances See [benchmark file](./bench_test.go) for details. The handler itself performs quite well compared to std-lib's handlers. It does no allocation: ``` goos: linux goarch: amd64 pkg: github.com/phsym/console-slog cpu: Intel(R) Core(TM) i5-6300U CPU @ 2.40GHz BenchmarkHandlers/dummy-4 128931026 8.732 ns/op 0 B/op 0 allocs/op BenchmarkHandlers/console-4 849837 1294 ns/op 0 B/op 0 allocs/op BenchmarkHandlers/std-text-4 542583 2097 ns/op 4 B/op 2 allocs/op BenchmarkHandlers/std-json-4 583784 1911 ns/op 120 B/op 3 allocs/op ``` However, the go 1.21.0 `slog.Logger` adds some overhead: ``` goos: linux goarch: amd64 pkg: github.com/phsym/console-slog cpu: Intel(R) Core(TM) i5-6300U CPU @ 2.40GHz BenchmarkLoggers/dummy-4 1239873 893.2 ns/op 128 B/op 1 allocs/op BenchmarkLoggers/console-4 483354 2338 ns/op 128 B/op 1 allocs/op BenchmarkLoggers/std-text-4 368828 3141 ns/op 132 B/op 3 allocs/op BenchmarkLoggers/std-json-4 393322 2909 ns/op 248 B/op 4 allocs/op ```console-slog-0.3.1/bench_test.go000066400000000000000000000040501455024307600165710ustar00rootroot00000000000000package console import ( "context" "errors" "io" "log/slog" "testing" "time" ) type DummyHandler struct{} func (*DummyHandler) Enabled(context.Context, slog.Level) bool { return true } func (*DummyHandler) Handle(context.Context, slog.Record) error { return nil } func (h *DummyHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return h } func (h *DummyHandler) WithGroup(name string) slog.Handler { return h } var handlers = []struct { name string hdl slog.Handler }{ {"dummy", &DummyHandler{}}, {"console", NewHandler(io.Discard, &HandlerOptions{Level: slog.LevelDebug, AddSource: false})}, {"std-text", slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: false})}, {"std-json", slog.NewJSONHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: false})}, } var attrs = []slog.Attr{ slog.String("foo", "bar"), slog.Int("int", 12), slog.Duration("dur", 3*time.Second), slog.Bool("bool", true), slog.Float64("float", 23.7), slog.Time("thetime", time.Now()), slog.Any("err", errors.New("yo")), slog.Group("empty"), slog.Group("group", slog.String("bar", "baz")), } var attrsAny = func() (a []any) { for _, attr := range attrs { a = append(a, attr) } return }() func BenchmarkHandlers(b *testing.B) { ctx := context.Background() rec := slog.NewRecord(time.Now(), slog.LevelInfo, "hello", 0) rec.AddAttrs(attrs...) for _, tc := range handlers { b.Run(tc.name, func(b *testing.B) { l := tc.hdl.WithAttrs(attrs).WithGroup("test").WithAttrs(attrs) // Warm-up _ = l.Handle(ctx, rec) b.ResetTimer() for i := 0; i < b.N; i++ { _ = l.Handle(ctx, rec) } }) } } func BenchmarkLoggers(b *testing.B) { for _, tc := range handlers { ctx := context.Background() b.Run(tc.name, func(b *testing.B) { l := slog.New(tc.hdl).With(attrsAny...).WithGroup("test").With(attrsAny...) // Warm-up l.LogAttrs(ctx, slog.LevelInfo, "hello", attrs...) b.ResetTimer() for i := 0; i < b.N; i++ { l.LogAttrs(ctx, slog.LevelInfo, "hello", attrs...) } }) } } console-slog-0.3.1/buffer.go000066400000000000000000000031651455024307600157320ustar00rootroot00000000000000package console import ( "io" "slices" "strconv" "time" ) type buffer []byte func (b *buffer) Grow(n int) { *b = slices.Grow(*b, n) } func (b *buffer) Bytes() []byte { return *b } func (b *buffer) String() string { return string(*b) } func (b *buffer) Len() int { return len(*b) } func (b *buffer) Cap() int { return cap(*b) } func (b *buffer) WriteTo(dst io.Writer) (int64, error) { l := len(*b) if l == 0 { return 0, nil } n, err := dst.Write(*b) if err != nil { return int64(n), err } if n < l { return int64(n), io.ErrShortWrite } b.Reset() return int64(n), nil } func (b *buffer) Reset() { *b = (*b)[:0] } func (b *buffer) Clone() buffer { return append(buffer(nil), *b...) } func (b *buffer) Clip() { *b = slices.Clip(*b) } func (b *buffer) copy(src *buffer) { if src.Len() > 0 { b.Append(src.Bytes()) } } func (b *buffer) Append(data []byte) { *b = append(*b, data...) } func (b *buffer) AppendString(s string) { *b = append(*b, s...) } // func (b *buffer) AppendQuotedString(s string) { // b.buff = strconv.AppendQuote(b.buff, s) // } func (b *buffer) AppendByte(byt byte) { *b = append(*b, byt) } func (b *buffer) AppendTime(t time.Time, format string) { *b = t.AppendFormat(*b, format) } func (b *buffer) AppendInt(i int64) { *b = strconv.AppendInt(*b, i, 10) } func (b *buffer) AppendUint(i uint64) { *b = strconv.AppendUint(*b, i, 10) } func (b *buffer) AppendFloat(i float64) { *b = strconv.AppendFloat(*b, i, 'g', -1, 64) } func (b *buffer) AppendBool(i bool) { *b = strconv.AppendBool(*b, i) } func (b *buffer) AppendDuration(d time.Duration) { *b = appendDuration(*b, d) } console-slog-0.3.1/buffer_test.go000066400000000000000000000055511455024307600167720ustar00rootroot00000000000000package console import ( "bytes" "errors" "io" "testing" "time" ) func TestBuffer_Append(t *testing.T) { b := new(buffer) AssertZero(t, b.Len()) b.AppendString("foobar") AssertEqual(t, 6, b.Len()) b.AppendString("baz") AssertEqual(t, 9, b.Len()) AssertEqual(t, "foobarbaz", b.String()) b.AppendByte('.') AssertEqual(t, 10, b.Len()) AssertEqual(t, "foobarbaz.", b.String()) b.AppendBool(true) b.AppendBool(false) b.AppendFloat(3.14) b.AppendInt(42) b.AppendUint(12) b.Append([]byte("foo")) b.AppendDuration(1 * time.Second) now := time.Now() b.AppendTime(now, time.RFC3339) AssertEqual(t, "foobarbaz.truefalse3.144212foo1s"+now.Format(time.RFC3339), b.String()) } func TestBuffer_WriteTo(t *testing.T) { dest := bytes.Buffer{} b := new(buffer) n, err := b.WriteTo(&dest) AssertNoError(t, err) AssertZero(t, n) b.AppendString("foobar") n, err = b.WriteTo(&dest) AssertEqual(t, len("foobar"), int(n)) AssertNoError(t, err) AssertEqual(t, "foobar", dest.String()) AssertZero(t, b.Len()) } func TestBuffer_Clone(t *testing.T) { b := new(buffer) b.AppendString("foobar") b2 := b.Clone() AssertEqual(t, b.String(), b2.String()) AssertNotEqual(t, &b.Bytes()[0], &b2.Bytes()[0]) } func TestBuffer_Copy(t *testing.T) { b := new(buffer) b.AppendString("foobar") b2 := new(buffer) b2.copy(b) AssertEqual(t, b.String(), b2.String()) AssertNotEqual(t, &b.Bytes()[0], &b2.Bytes()[0]) } func TestBuffer_Reset(t *testing.T) { b := new(buffer) b.AppendString("foobar") AssertEqual(t, "foobar", b.String()) AssertEqual(t, len("foobar"), b.Len()) bufCap := b.Cap() b.Reset() AssertZero(t, b.Len()) AssertEqual(t, bufCap, b.Cap()) } func TestBuffer_Grow(t *testing.T) { b := new(buffer) AssertZero(t, b.Cap()) b.Grow(12) AssertGreaterOrEqual(t, 12, b.Cap()) b.Grow(6) AssertGreaterOrEqual(t, 12, b.Cap()) b.Grow(24) AssertGreaterOrEqual(t, 24, b.Cap()) } func TestBuffer_Clip(t *testing.T) { b := new(buffer) b.AppendString("foobar") b.Grow(12) AssertGreaterOrEqual(t, 12, b.Cap()) b.Clip() AssertEqual(t, "foobar", b.String()) AssertEqual(t, len("foobar"), b.Cap()) } func TestBuffer_WriteTo_Err(t *testing.T) { w := writerFunc(func(b []byte) (int, error) { return 0, errors.New("nope") }) b := new(buffer) b.AppendString("foobar") _, err := b.WriteTo(w) AssertError(t, err) w = writerFunc(func(b []byte) (int, error) { return 0, nil }) _, err = b.WriteTo(w) AssertError(t, err) if !errors.Is(err, io.ErrShortWrite) { t.Fatalf("Expected io.ErrShortWrite, go %T", err) } } func BenchmarkBuffer(b *testing.B) { data := []byte("foobarbaz") b.Run("std", func(b *testing.B) { buf := bytes.Buffer{} for i := 0; i < b.N; i++ { buf.Write(data) buf.WriteByte('.') buf.Reset() } }) b.Run("buffer", func(b *testing.B) { buf := buffer{} for i := 0; i < b.N; i++ { buf.Append(data) buf.AppendByte('.') buf.Reset() } }) } console-slog-0.3.1/doc/000077500000000000000000000000001455024307600146725ustar00rootroot00000000000000console-slog-0.3.1/doc/img/000077500000000000000000000000001455024307600154465ustar00rootroot00000000000000console-slog-0.3.1/doc/img/output-with-source.png000066400000000000000000000672741455024307600220030ustar00rootroot00000000000000PNG  IHDRRxsBIT|dtEXtSoftwaregnome-screenshot>(tEXtCreation Timemer. 16 aot 2023 10:07:583x IDATx}tS VF˕@aA>v@f/ Eapfz.3nk:pNBЬ@ƝzԺZX,رyIX [޿V<{~<.2w²v@F拌`e͚5U+2222222_`A'`ݺu՟+2c5q7gMi~ٶ_`_4$/ٯ5nٖ,pV'כ>dddd>+(WѰf=%vF{Z]F1U! )"^DwTQMUN29B8- _4%GeP)ZCd$5r;F] Dwojaì˂TYgٟ~Q_ef >$nƮ⋜K_g+{|KS5g(?0iT%؆Ëa׭b4w ̩~_T"Jԧ.)Ua) +{G<߉=H_S9k8 ̮pQWYLA]UigsWtoԗ,I02Oddddd} ,5Q[xF lNP۪*o9˯jb%U1$}b!oyc$TրOb"Sy&ʍA.HwLPʪJb._*s k&<0q:.w)3/'\8B莹$ T8ṁ/ڸҾ|NoܞpN9o \zcC?yQDlep~hW$H8ݾbd#V/y@ ###) hi3ayL2V p#v 4Y?"8A>&E,o|c@Ձ(%d/]iV,%6TFN]_`VLD&h(4l=)s T!pc2oAn$@;8YPԬ7c#znr.%&P{;K1VM K=;kbVMHVgc;l{.ؾb]+AtV+9B`{34dbM/yi)v+mb %VZEHV'!b{. _SK^x-6v;> ( 0{D_MGq9`7?ӕPA(9 <}nʽ* ulo&ꜹn-eB=Omqh }3z$WgUR|T;n(Fu2% qTnRClNgz+jĊA&l5FM>=%Er`$h!)P HƈEFz ޱӓnD 00|GPF[,FzK/|>@H-;عs';ȴ@qsh_knab7%_޽1نrtCiG ,,qr0hÄYK0F#0يQ1%"/B4)H;tv3u eZ,5A"da*.h4 ++ &*X)N[d݄:&6S^Iky4(a$v۩.p&Xڽ=w F,^RLr)LHZ}ilvd/k:Jia&xYT 4ղv̑K3(&\evr5<]7%r͑ ۨ;!tjA8w+XZ)'d֓0K.߄G$`/T,"&Szh4GwZPemᗧr^UMZR+tx8:jWX+ {+!dddd>JTJMļ dA$cplg3^bqi$Qy

 wq!s-/ASkӍ0WYCQ!>W !UsuQ귐zcYIFmL(4iq]n9jʭ#]uMx[~)Gv5_C\iq~ruz%N*A@QVɬ`Xe`(3R88SaIQ aY6~Z^k{X;0)Ȓ5InZF hwfXjԨD2L S+K+Lsw HQVF ~Lo(Un9ox:D+A)?r*m[V>cCiANW2ciQ9ȣV98y$⹽l}"z&DTRğdUh,Ё^gqT ^gqƾyx8<3? fXf|] yYwS>d8YFFFμ>12#T"8(( e֟p()0>UJCTc2\wHOf*_aQ'|N6QdMZ~& bI &"u&?};_}y V!DOzlBoƔ≂uO{u+$o%X滑4S0+ PFe-#\s "|mZ>=:9ݘL9z V_ V# 2'#dNCN)4:]Z Y$E%|G]`N?X~\ffmw` hf3AFT ͣʎs# !ay}ET7k.mqg?}]%bc湧)=7"u}+ БBUnk,`>*_>D`, ^a¨/5cWstF4JäN4ΫzC.ykz,Eo{XYoճLoQ8((jd0`0A*>kT"b3,Ccu8fEKO[ףkPΈ^g:=S#, ԾcXv%k(4{f@_8& 9,X՗PrXl!6MJ(LϗM3Ռٶ2NM=-Qls(Lj;v}r"pE Oc5<$9Ԣ\`aReի@f@$ZUQS([ʍ@(T͓;-csrٟy??,iY@rwn\ j뎲OAN$ȯ؆#9˗O9k&yE[G'7Dhpf}h!+Dn77n#PPA)p_K,Y([j/کVʽ2 r7=/,~};_) Œwnexk{$ONՅ&i(ê0i4LeL?2222suf E#x_Fu5ñ.K&Zed (q~.ַا|؈ :pPG>Pp3+pu|gy/-/ǪYz(/â $3d%d^#Zioj"5,LO {C0gEF'턌Gj'_Dw %̧Cv6YQQd?9|q22CbA"L9(32}asedddddd>'C2222222222_p{!2XYf yݾEFFFFFFFfNȃOu(13,xMO )0YO݄QE`^/VbMEUftBz J79)iP&G}tJsQ(EV%$ :qJ ^fc}^.KIυ;7فaeA*B䬳wZ? ̨QkiIzN&q;5v_7i/U?Y JȻ\v th7#9~]3_ƒP%y]>G9U \DY7|uwL[9y qBC]t4cΏ`.=(XD}oS),wi;@mD,TVx˒!tvSY/I/Sy&ʍA.HwLPʪJbJKg HMUqFgR^ꗉY^. 1S\NuI_zt*zDCpry?px}J=g-w?ᇿ{`6?!1 c+k'´KBeA/ .1i Id&;ZF{G@LF2?:> + *=BSQp=(p54L5@K f< •_KXSf̥v X7(e Ls"nD(n/!{  S:Nb0]l-5rʀsѤTLdҏBГ_@qTyz:/\Xcdb̼۟wyo jeE={ދ4P|}lC9: 4Bȍ򻔙bW~OYu+9676L8c(: Й=><^"*DDOg'=SP&bQ3$B2I󮰲1 a|$I6Ma8jc>5(dLݎݾN1~YJFba=޹"4y,^RLr)LHZgaEG|4Gjh;͵?0{c  ]ٵm4T<\imeہTbyBf1}lSQ!u8ґMև$|qд9E$hT5eml@>{@\*~WFFFFƂSPRl"mH$$Xc;4N'鏢ȫੇd ⑅jOM7Hֆ69cYCQ!>W !Usu{KBv1g USRcioAYܽd\xφ`|g<_>iPݮ&kbS~+-NOPݭ @ (h:j0u 뽌 e&V3gw5=3009,+fTBOkbP}k&4Y&MwPHZ KMPWDcaeii o(Un9ox:D+X=H BlPm ܇7sy(-<6Us3:~O(ZՓZZFQGő"⏲%ur{x{ExAW! TrP͆ q#gLs8tWsl؆c 5qIOFFFFǂ lO:szhC*!F<~_c-q`[?a3;v~N425vE[pio$0Q^]Il$L(PD7] aV\pfj__桏„ݦaӾzo3RL)Vv~O?{Jt/R$J%$d e >)CaJ,9L#2X[P*tzt!ݶVμV h}Wk|Js" ` MUJCsot+WWXq 9_hӅM4FfxXEƯHh_zW_a^U>;Л1e򱿠x`S^1I?'?*``t`bNp;~>H~H-20QYnA+s._ρɝY'XLj'eh8DAY>nԒ*m, z>h zc#Xg 'NR+ıq+{0ӯfwdQln}&doxC'####c^9֪k4ֻ? zqބ^?ˬ?# NMf*_P!(JrJ0h&(PKg/}hAߺւ"5iTw3g^yW\' z4uΩOyPޟ9%j)D˕]9}W@ե^/N̫>#2QMME\efFC-w1 `}tF'`P@CNN^bx$w!֟qT??5EB !Ȧ`,\OHr J5ZWD6asq|W9R4/By\z@rWBj̧^)ȫzCWVOϐ$J({{(YomV|5AݝqfWQ&vI zVb 1},Dy`LЁ5+d.z J@3I R8-X\EW`6w kn*-\|QK5ERf@&XW2eK1fWDa<25MP,Ř'û+{e?|JY*n& e%:įeM&Q6{RZk 6~K2N`+r9XLiN>e1_)gZ"*-?(d?W2:=W=.d)$;_QQxo}>-Iw;99}6 R=i Tyl>8Gg(VUsQgܭ5u`k˛i[EV0wm8m?[U[)H*7Ru!V VB3F`YC7fSٵWdqR'?O9(0c]E)h)Y9p!mQ](nqEp{j4Ydx%g/.76"5L2$i;O~JX1ݯp5L,j˶`ςpIg_& Wsw7wkNpDJIrc9H/Wޙ>,~/{@woz$ώiEE K,+  c} +Vo=HqI&teXs ,sodE |/ݒd.\N'Eãfdx$u$Eo|6`/q}]}v&^9}7'h G}c8ѶfܻD4@Z8{ӎ:97uɵһB~>z^ ͸@Tգ!J_We,gv#qp1[DC7pk#Br2̧EF'턌Gj'_Dwiu4慊I{|'###E`$`(&KP8JRc2 ?5Ge`!\kT܇?7{ddddȃ! J r>32d!#fo;I{p|'###D3-Iedd`e͚5Uo22 :uQbVgY30ζTc>O9S`X N^\.z', .orTSfգfLx=l2P(!KH2mutr;F] Dwojaì˂TYgٟ~Q_efL447-JMvj:/ٱojLc4ً[ ?"< Ey~N^IG˝zo ߺoFwI߼GųRP(+R^ k: ^xzGؾ*Yq|3ޘ"Q8Rޏ+#MWJFFFFS} ,5Q[xF lNP۪*o9˯jb%U1$}b!oyc$TրKT~rc70ةҒ1~Yaɸ HMUqFgR^ꗉY^. 1V\NuI_*zDC|waהo#l|z9?|IwrͿܞSK/sL+BaٲɎ.ӣQpݨuZ $N w0ePBO6Ct>z*|l7=(p54L5@K f< •_v#v 4Y?"8A>&E,Ӟ|c@Ձ(%d/ iV,%6TFN]_ù\g_&2GCI`sI/0I^Ub 1 3~9%xlߎWrmtvށgZ* O;q_sh DEE&f_ Ȃ^&Kx$DO{+釳̱NjM`+vy0="t] Zۛwso'+ElzxK{Li \gn=LH }`:DLB~%SK/&)drq? UFƯP龾+Pqɡk~dGW })FC6N>ȫ ]D3ڭC4xg8YD V!| @"g&dgi1u-C!}[>m뚽* u&plBy;]GGan-%WPs`Ǻ<-BѾG@H-;عs';ȴ@qsh_knab7%_tOc -̧,RAo<i -:ި&;'ײ 1 ga*.'<tf+F!@`$ Ѥ I%IhX teҼ+n̂x`4_;Inz vd`(ژOy& '7gcŪS_VҠGع{wnhznǨHw;R^txI1=1"i%Av.| r!N.*T+ޠ/ ߵ͵8gK'r͑ ۨ;;)hR,eWBH8 q1l⁃-}[vXA0{ hT`]ől>}ilvd/k:Jia,|"4em9rkq23oeOFFFFfYs JMļ dA$cplg3^bqi$Qy

 wq!s-/ASkӍ0,d E\-ԇ@_("VU?S/~ 1|iԶj4>^j B3M>w ,UmKNoXV/Z%\ff1|A{v\o ҿM0j5B ؔJeuwwP5 ڤ:LfCz/C: emmٝbnL:: gxˊZ#mڱ M!EIrԬ6R@3RF&aјrXYZAEe'0aA|W$&ʟ_vɟ׌M-B} u,䓫Fs rW`lĕиR *Nėukl&w!g:DF`Hq.83g`yuIG P.Wpuxr@%.ȶ-+p̱&xӕmlCvn6Dݸ]H<gw־'?gA'Ywq9=!Ř3~_c-q`[~WnwPcP%4hdjL닶ZH0.`,,h'IP${oN!gì6zuNԾ il?ޡ֌JiH\~ 7|My֗$DyO{ݫ[y>&g\<+|r)ߎ(ͭћn\WÃV~loՔ]g8m`(܂W.9C`]6-;NN4G7&O(ip89dֵf= MgvI- 6"P񯙕׸U]7U 9*fߑ}8G9 m%9gxgnDIjœ86n!q1TF|}'####+ZrzSAo)ބ^?ˬ?# NMf*_PwFJrJ0h&(PKg/}hAߺւ"5iTw3g^yW\' z4uΩg"8[Rq? .[_yob;_g76LςBK'U_j1W7|Fe"(b0q|[ b*4@@o:V# O3>)(t4:I #~<9t&GHïgyOx q8E"&qXY=tPb=yE_";Mn. _C MAοʑ=x D6/ Ծ7dddddU`![ĵzz=zx,嬷Y7(_zswgYUyUQh5c20 VbIkU`*:X"CmLGנ^2u#, ԾcXv%k(4f`jp:<) 3M,H"rrt^Is]INN8mz~_A7s&>58sƌh5c̖5(")6{BZk ("PvvDA +jyIs&E۹GfWk!O~hW=FMͣl)7a HJ0׿%~?YHSGz@nͦ?]{E>5]Л͜oRm?[sɵod_m1 6fI89}6@mvQAmQf:7gV{rS] P\ggjLsCf+(-5+'\S ļ 4-FсqIFx$m ÏswS+NQ]MyY0"=8L˄}}jr~.ͥG'?ҿ;;oxyuΟW];}{'|5iEE K,+  c} +Vo=Hqy&teXs ,sodEƗPnIj2`.vQbãfdx$uq)حZ`gN譟9EQvw]@ɑC8-q5Б_1B'紛'W>rs~i+[2) 8<}{474ӽ"_jBF<Ƕa Dtq/]͸@Tգ!J_We,gvyTL,=ʶɒ$$!|MCe`!\kT܇?7{dddd>ȃ! J r>32d>|Ľ#}u?i{dddd>C2222222222_pfBef*2222222@FDgun:J7|*Z|g)0YO݄QE`^/VbMEUftBz J79)iP&G}tJsQ(EV%$ :qJ ^fc}^.KIυ;7فaeA*B䬳wZ? ̨QkiIzN&q;5v_7l5;o\ ~ȯzco'HkQ.;+o84~ksg}𳓭 xq^ r-%UBכd/VY֭ݼ"RuJbLQK]nyL`RYENGPB ʻyCNξrX^BEDWez)V]4%? xG'>q-\Rev ˔ x.1yM `eV3) sҏHJ!}S꓇G)H:95]j/qb]+ 5Ct^r1HۚggLfa- z 1;޲Gye)BС IDAT!o/mmy`ŊA︀3霢RQ.6FaHKɅ`>EY&}9F|C]S\xeRs#m?AY1YG3eyQyls6UeoDm=޿#HTHBiҦf~fӬ'dKиhOD(ǒO.Ņ/hPX׍_Av~M]ѤЋ~ܽn!HvI9!aB+t4&24s=)>nĜ \7Ҟ_VY@/ؘ=5TH}":}6%)ΩS2A,2t ,7+4anֈAcD%9%Z]24ˈKp;H,؁o-¹P?{![2jii)dʒ?EMmNVq*յ<jݖo|#^{w~#}'8Rđjj;쥦0XGLNtOU ljG9ɍ*̻PU>kA:9{(%`׏Xedmw_@p3SH?Fv =p ?flZX=,򱍇=xUwcՔ^[x1c- u/¼|/0FڗP@6_9"#BQ>\(IU}^\"z^\)_6o?2pwD/~8qX,SPTh ଏ$T ` $[M4$M 1?F$_'gߍW1v}ͯc+M"0|}3  'dQ@WO:/hXS/|˩58%#r*(Vwz Ws(m_rD>KW@EP>6LLL)0&OJwS'6[&QJ( d+|{Pظ-7iu]e dh"+?Ŋ}&2gx>ڻCFڷqL4#ȕ*dqN"zm{vbwt5=zwpq>PZٵ} 84<'ZUͱVe!q9XӇ8tS=/f]X)]Um #ǭX_\\FEQ@xj뾇=Oh!BQ,Rʟ*r͝k$_flHM=DG~@[{VDhw#Ϫs+ سjGsMmN"_6pqp~p+Eܞ?rd(av6ėzi'N/&T(4xրV1?'ZgJ_T"* 8%B^:XI%dii_^NV7m0"m.224jpQk4@) @!"F˘Y5Gw0Ø$ğ~Z.B׹1ggBl֭ r'~O=]Gϼϼܻ0gjlmNߎ!WgnMlq܈X# \"3[x\L.IX@feEa4s5~>=B eZ wѥ^ #lPC/$'Ɖs-)79 hL KxNBk F>%wwƙW_EՀPIԒ[}Yq@ EEVr.([0%?բ(AG "| t9)cu7}aF>N|cs-. _8w\8S01\"<ޒcwx>TqK^dQc5q &,9S;!9V+zTH#T5%R? 1B"NVB v 1-'S1EhuPf4R":} H]Ķ\'D 4^TCu 43OjDz HtŮ۱d4Ʌ!8)Tfb-4Oݒf'7 ٩]g8™뫰TWo@*OzxeAjG9ḁlZV*~BLi~e-}^\"ϒ XUj+kęuǰ*DGvEjE8{t}42k gفX, |NbJ5mc L Oo$E+W6GX~,nyD^9(Pr [<_8q|XS)#BIeFtԽ g= |C*S?0NQHA|ם)O=Ѐh`ɊJ@k' da%O ޲o;E혓a禭Y"c/r]\\$"`2]1mﴯW5+fvL.v0 i|_MisY9Y>7}>ҌwS0a2nbZ64@RkMkc#bLi:R\\r`L"#] #oIj@'v-joߖݵ߽7Ǡǐ"1JV>_{9|m+ۧ|u^*9lqq=Sj^mpM-MOqlokNO{ 'G:ܷ"y;PQE-}VoqкΧkHOO2 Y*5|*i-3uw|峮x狁ҧo_20\RDsb?o/*fPP5? t,hRQH]\3mQȷZ)H#YqatCiV&YAFsjT5e㔭o[>̡̩rff2%z_kaORikf/g=Ӿe0[ lGνǯHv c,|qֳ"r}:m'?K^̋8pA yy>*V٩v[pI5Q{B/ QqL'~>Ybmi~>U1-ĢQ"AO~[[vT9j>o|*2d,}^;H\K eY~u+%z72oSRW?7 +Y;4|*l2~b^x)PQ$ջ0@^U7 ۥgIHxܳqja44~W>FhWc[ *R\xcR߀/rj1*M "iToǢgXBoSSx5iwCJ |ĉ'EZ>yhn1`|$T;_9+w)ֵr[f%i54J_&2v8i9Nf 0n)6j 3 :+XE,};r,Z®82bZVzd^z acB#9Cr rF3dΞWQ!c!&uWsM]ԦbF)*Eym;OS[Zc-da'QDD]Q,eVr"At6fs[=?,E36lX1pN<dԈ:O>.5-T)N|}9F|C]RU{Y(4`ޕj:#FjmT:-xOPVu G3eyQy#G[x_1v_Ooj69Bl{4a dW?Rιz#esقG<'85v3=r/nE?P3~|E N|.~MϾAsXKxǷ0v c2ӱTb6 ክ [-XL* 3uٸu*x7IKaOwvB/<~Cu z +FBG<{t61R '™hyb=!w1w'WHFI7"ZK>| )b:$?"dR _;W4raE!a]r| _Fzf4wN&RQ_ツt>/ ~gc4dAxYr}Ī_ZO[أYy-4gxyZ݅z Ӻ-X3Ф}nظ #dنQ)@ Y5:+m@Ug^Sg!ǬGMuN?LAшfG=g01jȉlaԯYʌgV-1$\3Cf?!\@W=#qF{$q-iR\F EzdguM &љd&BJZo"C3cFPXLxo旕a+&6fO풛ʟb1ixODϦ29uq:Yf3"vsf3z 9ۈ 7[ۨ|| `ҾHC"nkA:9{(%`v/sBK&OQSvU ku-ƶl"گa_ed|WjҳN߈`-A0M_.'xXjLu!g-[7M]jAFwd$%ğ0E> |o|ֽV|^qLC {C<* ٘|ڂh#3J`y/ }fJ+lF| lcȒg~ʿ;; 댞9Fa.cU(~?^ңl arv(dDq$Jvc, u;83b-cǸ?QBn6⇃{( _?o3cu?BmJ.D:ec+$\|k$$39g53h?֓z%eLeo?,>bկrWT^j=ٺ #ŒZ8[8luty%R7RZVNIZ.SPTh ଏ$T ` $[M4$M 1?F$_'gSab_V7D*af;=dQ@WO:/hXy,Bw)Z}S*?*bu7xAPϐ?i Ve,yW>@r/ne:tq^.5PTDk~WJ(H*7h0k%׊t24)s|05xȠA`0}H- G t}lPS`L"72WOmi 9`Kz۱h]ģʷ(~k-:M%X9LB!cxEnQ=@'G[j9^6-Y\?cȴDZf^lgd~Z{dfqlUGex2![VG} =۬fxf7'yOoh| {JIω!MD́V"=AL\n3dTf57GCLLeD _tn/ZEMIwBeA]K&L5О٢QW4\#ٚmLE"sI#uBV<_ P Huzg)ߊ>*S9VcvyĖT׾\]>rs>N.>Bׂ)O}rg`?eq r*"7hU̻ ~w IDAT3~wiETi1Yl6㌚5XwSiPI._m`عlǢ|]>2$B@AHGMWqyU] ?c1`fC}us+NѺBf5!Q3 a$ a Kaaxtl.26<厗_XX!4$n]v٠i~)hL11O+Oz>dSaCmu=afY.;H<^=ɐpdwN{z;83=GAz:{&՛"kXn Q`Y ,SqmB(J g,囇kSHW2R[~1%>ǰ~?kS?BiH I>7]9q;>@ {K{N_0wGQWSHr> #t<ΙŪf1|у$ rxґZIa-cK ?ppwC+ff}|ʭ:OkfA|nKekՅ(K det_/}-mHZ^Mbrd |7%*' J D!|CQ zW@Fw4>ⶽ/._@`Rk5ݑ_A[)Q5haNHwhp7CrGcڗ_Kctɏxo)<4guY,#[$) #,nM#) O!C)XZfi٘ӆtD:&]CACP(Qƾ`͌ sZ3}q22~/zs̏ek)4GvT{2°4b>#$Pc(4xրV1?'ZgJ_T"* 8%B^:XI%dii_^NV7m0"mj*u$J䏘r}싨30-d rG~9>)doIܹF{Az- F 6ʮYgKo?Y[vcYmƧo?,s䵏X׾('Ð] ) `&!jhO 0T׫Ԣ/ڲRShZԻrg[/K(5iIզc)E5qsW_EbMt:t뱖oDt%3}q J.ZbΩ`sF%#}Īֿw9jLX uL/cPO`y=^M }cёV5dZ%=v9S)#BIeFtԽ g= |C*S?0NQHA|ם)O=Ѐh` aF-Z 7m ǰ|%-r,R_x˾_eW/W0h}5EFedtH3Ml&i٘IE=v9hʊ1Hpqɥg19鮆Ʒ$5axP ț`EQC{9,lkx=M&d˞vxgjW[8\SKxNXj=mcm\ݸ1BMXj8^qVCN8xjCvwceo$[%z(^~I<6.'RvܜojoKC][-Vu0RRrI#'^@锷H3bsH^R6yO+(rK𿜄[.; yѱ!h?KBs*upo?ӿHjEqWHg=BsgIشXZѤEOe򩗹%]gQGvUYPҕȰSyT)D.Swy7^;\'$u-mz\ɘ*M8q|>12ר[ƅZꏬ-՜^|l{7SV;ĉ3?ŠAǵ3JqrUUVDO!gu}k|U gNA8_@ i(2(tEXtCreation Timemer. 16 aot 2023 09:59:57U IDATxT[%k¢(lJ X ɡ!w&.d8ԜrO9m5=Lؽ74+){Z3xOD̀dj Dd?̯}y_0TTTT>ho0iK1Vx4>i[RnF!¬gO3WfoSh1ǡ sp`QjK"Wضi#qB9n;FBEVEc o5mCƦO!ٲH3Ad݉ߐFTd$ > ( T4qlH[LғTXLzs5ˡqoҿ-h/0W1h>~=XK^ ߵz)U4'Cw: mv>ΦCJhhhk"eg{h׾YCKNf]HTTTTT~v"?r&";vY3[>=VҲ/ѫȞ3x}‚l[0iE՟.(ͩ7 $SZVJh }J9A(gG~lYc(K'8##ꃊbWniV-o ǓQTBYTqnoMtiHIA|W"p;\"{ZԿ=M;_C!Z̫k D}F_MBUTTT>v"hkyc"4%8蝁M0&]#^Nwm+IۻctiXvf^\fǝ?c!|DGXxllB7 #nBFry#>{/YOl"~<^rґD%"Z\)@cCnCb3$.b9@mUfBtN]}a+!U^F\Y(/JxĢ:ƶtMy}mALMUE|ٿtt~:&nV&[o<~\aKaU"mY\׆DD:o#+@HV^դA{ǣ#4R$QOf8vyfU wQ<@LjH 6־C**** n;]F@8@ hI6w &񆷒,teȲ&#̎/gL05Y0= fd`sjM^41gTO]dnNu+h\~-  m<:,aZP+,֚$S&{wH.Xvi~\p}O>3 VM~50'Ѷ읨m1/"+kD"Rm73YY}\4aɁ[5ENvfM!TV^{aˀ,N~eb̍θr:n/(.xk1|ot D0NUTTXܵD4?D`#hAlO2EC`K[Ѥ@7ۭݸ,:llvœ.BGڽ1Y+\y=6ڴ7!:b^\&# V8_Nzg*꜎ 9VCui&d֐ʡQ^DK476#Rڀ꣢ǸD}yo8Ɇ͘ ۩ sI`%ԯ}|EBr+/on~u=͒ q˻3qΌ4#r3 33tEF~X-[I3-z>woKh|+hVdl_C2%Z}d_ދ۰=fe(fZ$}r;#*Z~qNl)[qs )*/%F;#WBim{x4=nY-~KػQ ȥF}ieΌ2sгkG.b Y YQr]#ȵ\oyh^YkDCaan$!1Ȣ/t.IT[]*) a»aO SU'3]cd^BAAU;1:߶& ''DaCy%EW Q̗!=bFEu\/I^"d0A_̚ꈛ)?MGIO2w>aDZg;NDvQuKA>@Nـ} 3^/ՐL%it]ل!fn`rk,E797?tz4=K p{G?4]P9=)n#.y£ϰ'IJG]1M[ClJr'kD] ܘf Zj+ewpyA0fO qه֯\d"ߍrW֕MͯmG1in_.glK FָOEEEs%]Fiou.KS>3%ù: Q;˨7 o"E#333K/\OA+VZֿ||i//oh 21E_^Q@ZM_?2Ō@jM|}_+V6.\W5+TMNN*8]  `ЭMM0fs~t21͉A&] 6\9s\B7i3۫3OsB)w_.a&Bդ踑mHMAzr9 NJ%|t{>MIz !쁏 )"_dGfAs kލs)TTTT>%(NqxQńP(" a+FiUukIÔDRnF-?pK`ʗJJ`lXf0|x("`LFEF ,bQ>7ʦOcڂ-+SRi[$* #g=e)SO%vK& [I![dQV05vMo~|ѥY3"]:tȺ8辂#j9Bv0nFٌVBMAꤘ,"[f ))~_I9hOR&LU5{h[udHN&SBq=~v`lRS]r+D͕4KJmϺw~?z9XUys_aQk"@3$iV`a^/49"eSg~ɋ<_ ?+9;n{9?ছ?CWeOxhCvUTTTTTTnrۉhF<>drl:gShW oXRJF"{^  FmEVȫ'Wd7lg, WLiY)-K_A(gG~lYc(K'8##_(niV-o ǓQTBYTӇ5JХ#%pSg{?ԑXJU_Вv:Nԧ~{4,/ j~6 FE3ue~Oҥ'|RsJi>r QQQQQQQG[[̓-A dn7^s{o[)LG{Hò6*HO(0;sd$ϕ42[*[Ѝs4ygBFry#>{/YOl"~\僙>|EO'?o\nolC FNmpG} Vk9 8C 88'6pbsY;iVsڪ"&8Cu(I~:of`)547QӰtt~:\X3TshxuR#m f\Dr!*)08Q&Eooh9)T'\hy@RRG}D9!&{8v'+h\|*****}Qp@Фl;&IO7n_,k2¬gnU=5d4q#S +?M)޹g}]\6TK6~-  m<ޅh)޽KК$ݳ=bgI.&t9NJ0v]~SN_ s{4?iY+!lٱ3˖{S{$c4Ui}4`F'  Jt4z7=SeVVTHm$모oC']+!tأt#vԟHy#Oox=x}aBRK"]}V9{AF9}\wUn\?Md2??oߧ۸44DЙޙ@%:yP]ɡQm<]2 $rfum(adwL45KH/,"s%aIuq`9NJ$9Na&C09b ƿ>|b?o>p>ΒjluKOA|*****J"U4A/ۗndf ?dW"E,6lYj]HL.*ч(/3fwP 9Ro3P%4h k(&NsNɆowc,-bG b\Tr%F;3ˍl=vR(q+ߐEMP #r]5Wl3Xrc?_ 5wb:qpIBv'!/ VI*O/u*OD!DC Ab n;py-OΡt]f%uUrotzqOQ|D4lE!ތI z ޾6trcz/g (IsM Dv)o@~E4b; 3Aeac|LIėW@4 t٘2^ڑTQ@ c/^Oq! 1(q)ÚqpZtMKO;]%9w&5+HNN*uQ'îhA;RSH91D(s’ټ)8yi<9艙lN|":fRo 񩨨|D4dI>Μ"/""pSE[D4lE/ ѽ.’Br-iHJۂTV@r͈E21z LYI  " K,FOEDрɈH>:;24UdиWƴ[V:$ҶHTHG,9zR*u3& [I![dQV05v0.-ť`B./VH/vyL!OgA~wGMx7qk"`3(idX F*mdXm%4Ncs9kldF2fw0<JREK$n]W:9'zUyfqP%[+QW%a5bΣd3i/ߪ$Drl‰y]CgۜOò)OQŦs Pr$i'DdUh?X 1 hA|QNv3p`rAaq0s>ʏ>mkuk]_NQNq0s%QIa)x| ˏ+"_'h g^ާLu˼L˳svG[r:@+s=~<=7cg}ɗm̩%$bwZꨎ9~P5uWP[z'pplFӉVOsm*who+Z8fkᄝivoo x jtu2d"ȩGy?=4;ȍu;MOXw ~$mg(n||'99ʝ5֔/Jj9qӎEo*]**CtJ0'n`CJk$g&IOEEEE%];GTEEDD-6QD˘} _ESQQQQԩyO uj^EEEEEEEESAMDUT>J`9@zj^#|dc<s\`Z=^r疖a &p<E%JOjyT8[]Z1RRҜQ]jEF0gG5۟KGG4\E޺q(/SïCv' ǜ ,JDSEebթUn;uC&WA(WFq'<~Ϙs5$#$42[*[Ѝs4ygBFry#>{/YOl"~|31Ή6 k/! c|5AZe۵%!SW<іb& ]!RJꨯ1'b p4r|`a4o_7w`7SSUf(9=<1Z(Iŀ zO]'5T x}󷌊ٳ!}ZCë_'5`J$܁uirFh9@mUfBtNB <'ej6(eWASQQQQsֈj &d#x1I׍7O|q2dYf=xܬ~z|8bFƹ L)41ǫx.}YvqL S-hpJDl4޺XR{+\ 5IA0^=l:i}0y f̺LrLN]j $~0tLdj*k8ʤi]y'4Ք`+e!3INR* wsː)/۾S+ݴTHm$모oC';B:{t~Wn)zc4Ui}4`5@Ja%Wm:yBr\ʵO.>Ovh~PC!@GІ $ٞdeׇfϣI/3o[Qtռ _xe(fktu1v7fU=qm8h+!|UO}L͂s[^{85 #lEZr)4p ÚT <&cc/p?9.nS+4:>?Uxw,rC&f<=7!&8B_u;Kmaw~}in8s #@8\yfˌXO]2F;&a%A;9;.9.L;'I)ޙ@%:yP]ɡ͉wp3I1zħpWѬ:xپt#K$0K `q%X'zA/bɷa{PB"dvQa>GxETm'6ݭɹxr+AKp4]C6y=dd 48/㢒#/1ߙ^fnLt#\1mw.:lW᜼UnY+t{_< Zj+\>i 7S#5c^E#R!v@zTTTTT>hlEQ[xbGƘhJ^x'Ly+AXmfff/d1 E,_S>0cBOGL"I(ƄqҎ4צj"V8k5}X'~ alAY] KZ Р |\xu~iKߺ']j#JH&R&r2ذL6'Nco>My3kX[ebP"̅%ĻkW(T٢ijxS#QA.׿r[C|*****]n;M/Eq3Ƌ(&ԇ.@F1 [Q6K7wtjK\K$Ҷ t3hLs^SDVRf7˅QDQ4c2(024UdиWƴ[V:$ҶHTHG,9zR*u3& [I![dQV05v0.-rw6n 3x6:E-7]`2QH޿ȯ_ߨCQ3? !;" $Drly쮕̤=W:9'zUyfq䐝c-.̻ii6cPpR}UIX)(/ٌaōVW} SQQQQdͩ4, Uly?@n Ѯ/GvM@^cي0C6Tk7C &Wz$ 30ѡ#OaWysQ s>7}] ȍ`·{-88"%+qppnZ}0~?}} _V9<˟t??ܺt[Q+<|~2'q'h;}gw63Kc1'Oo^xp;RN{c=hsOP麟LOe8TMݕ짹 \=Vp%(SZ~&{hw3+>TTTTT>INNi颳5֔/Jj.$OEEEz׼ :k U%y7!%5 }ۧEᮝ#9BD-6QD˘} '?_|APUTTTTTTTT>ԩyO5UQQQQ6ټoUTTT>Ѥ-[M$n1!au}12{B1>mx{3v#+Տ.\Ibۦ a|n8EqdQMЭlH,Ҍq;bŮ% iJEXLFsm?MOSw˻Vtk$=IԩWhYzS9}9ȇr~p&] b s`ks1}LPs/z.oN.|YC+\NDmm1OzLd<ƶ3 Fߤk$zA2m0y{ww!  Ы Q?y츓s?g9ךOPz --YF9sȼҳeo!#9[ƼO'soOA?*7IrґD%"%ys4|WwVraw(kfj ,5RuuinlWB'5`J$܁uirF~:g릟 <'ej6(SJꨯ1'b pĴoC5 )HG麟ͅ:{M57뗋Ok!op>xМK.puS; \lz}R}#9a:y{V|߼W?FzV ]?"ZOMc~S?tѯ'G|ŞC2N4]mj>>~/0¦x_]t]yR_<)p_NL{&G)Ba??7ƙu Ko {Jٲ @4~UTTܵD4?D`#hAlO2EC`K[Ѥ@7ۭ(j^]?5 snۺ7 z Irqt1WB*+%z5rHϷ">%4΍qj܃F-ي(/)gShH4K7]U[n&\!^K2oL^2DSN#yc^Lв粒8h|ENu6Kҙh=:،tJiWyaIuq`9NJDT*99&@Tʽx?M3CF}x*K4W"X@~qgv,ᵼ>fYضm< t\WQQ$YOt}FH`@ jJN~z/^ĒoօD$}r;#*Z~qNl)[qs )*/%F;#WBim{x4=nY-~KػQ ȥF}ieΌ2sгkG.b Y YQrbU0_Ϗ^.Ò5!'\e貺.Cװk0̩BC8Ct 6Љ["m'.w9tqˬ [N:́R/.I*/4r1ODķ*\SF & k9[% ˓>bDtX%r~z@&]}c?gWnZCWk {9***_|8*E~7cDa-^}&\yf^ǫ!Jr_嵳 %C&WƂ-_kss3vgv-" ޘIj/OmI\b<لޠ҆h4Lqwɋ}=ݼLZUW:*F?K>3u9dֈF|=t~%.ݍv* jsu]uMu\l9y8Vs͡g^)f]Լ2VkZ+ˡWt9^EEs%]Fiou.KS>3%ù: Q;˨7 o"E',8/\OA+VZֿ||i//oh 21E_^Q@ZM_?2ŌvjM|}_+V*ôHxo$R A4M0fs~31͉A&]]B䤂ӵjQ8NgCN6HTn8~}L6'Nco8s23o&UIa&B՘3 ̯9`6ss[Q f.|(_~450B2y|tiܝ+}u[pN***[n@]'8sz`(bM}dQlIӰe4psGt K ɵaJJ")m RYA7#8%0Kd%%`0Xl6,q\UK~cY:q <WNhĔ:ý`[#i7JmaVbS9Z`wC`v}9hwt*V >(ZRrAaq0s>ʏ>mkuk]_NQNq0s%QIaEui366F a%[iFbOY*&͔bnETMdrٙwvҩMM4Ĉ&v`e޵IX HlcǦhipZ~M6穢>>=~g/ '^/ p`^q[4~<ɓm!^C|%z”mǂT?=V[Βu;KWkP! Sn[Y-쭪>ء6^*+pJpjҼ^J#z|\R>ƾ='ٽu|k8Nў^(|}1Sy;۷3f=f#uIDATjZ^D~DC<uboȜ[5' ?|,>y͖2V1ǡ:9g9I~&rVr. Np6۰ʌAQaF#8 O_wMdj ~<6jl2C,I_ k!ifq.l;曋6Syw nrɒOʳ \ j&8TJ>{|g(y#j47;X-" ϼ~xl N3}3ka;eT=[ό8#'sXj߯h:|&X7|MM1bJrPk9^{%6)w3ܥz]ncG Cc0J3{s/}Pԥa5Nmf"#+1Bg/ڿ}F·OnZ̤>Dn>Y ]>lRbn6;($֚C CQλ}nOV>&'|N&7N#uxqZ~zF+_&N'hLHdLq\ m B#NXnp` s4hO.D] |:I!27TXCֆtl893og5PmKb}ˌi?FI<֒lkcݼOf /V…-'9_-l%YHݾ*Zn|Nw͊‹+eًӃmoUz"r?+g8?|4( f2_ "zyB k\'~u' \B":`kld4]k~&8B{plVlsթh_- q*bxೣjZDE^Qh{-is{h8ry_ nNaw~QC]/@9 ΍T q\V'?ݥhFR ~gmdt@> j@֩4,0h=jQn$͇o2v[cܶ"!?mmmDU/%*?mU!zϊkd8ơ`mSyPM5/6ޕ^>j`hqcۏy1׋͟9z?Hʇ |/&{^4ĥf`ln-!'b=Zyhuj/4p{t$毢̊&z~Cs؎70Y'5K{ُ3t^2Sh>h?+o> k č" ^E4;12č2>Ić Y %i ~Y_;S4;wϟ_=qᢟ2_FREuch [߾ w?zQ[)E)It[S{՟)Cj|$_|23߱DuI7>6`ޟfJBYa#T@QI)`j <3~g !hD&3MN;N0]#Syt@^tm3m:t#e>uMtWnlqSn&j=`̱b--!N1Rq[HfN %? #9q z׍+"ȳR^i%BQ }'=|V-% @qװ#M?>,m-fUm5Xl9R6/na?Z e)\Y[x#XOVu.S2t}Yqm o.'%Cɓ3[lrB_qnwڵq7^Ե}t|>YF|쟐IFwCW}'F =V~ZڿESu~-ힺhͣ㈩;rpW47Y;߾TW~dJ"?z жȜu3gk* ўsk_}qD3c 7?K+Ś~.߲Pg*ǽ23 +dJ:@7"tkڨˉuֈ;_#}i*e y6 Le?N?ȉwZtF-XVeZ2[7R9i\z:T4YuX iHYAaY t?j4z ck!512t!gO7c()dn\˛aL蜠ܾVF~7.ksXalGZ>e ,:u&|0T{Cu ə9]b2/ UƮkHR//qKŀs5 ,˒1D_F5w ZYY°>rCwy&i+H>LD(萋>H2> i ^aL)^r; |m/E\ _*s.Mc4aCb1q0@x)2[4/a['0$פƟ f&_Q+!@!wT=b+17A4/`xa$! 9cQHȡj\C[,LeT֏{eՉ7?K{wqg_3Ag'd K`7=)9y?)' jtjrj{O;fVƲ$O3o9qC͠Yi›S=F-[WE{/ gff鴄 3ж b_g'SV F>Ixcu7f 5f ][- 7ڦ6W}6lxWHNYsG|v`*|Yβ\ ' V;z.^4|}rJ[LzfޯBFWPݞRGfGKQtmÔrSҼ`"?D=:A:?]>D~\)Q322B%J^'\]7P3lNˑ= bXn;@%s y S T tY D Blͱi-~24T_!g,׎{ןdm$?](ÌG!O?w/O%?#s"&/wDO.bPG/wEO@Gbܳ4))2y9wI ވhhL;.Cj!EiFIX 3̯MaE rҒ0Lc-- 1SDts Sz4~.woH01hh$A:2V`M3/'UBeyScHl2J58gmlTsu,9BiQ&𲻔\|ȟ7Z ʒKbZ@Lb j~MbR'N }l툺ȩv>/5ȓ?Sgu;nM'9 }-Ev\b2.2VXHOb͋[(:,2wo yČ% /&3 ߶G$䷯8QI'bYAͅNH4&r ҘDfm7q eO3؊W% =('_As Vm't!GWgOR>낥FB+W2i_32y) W0D8͈_X'3_Q60N7gb٣럌?2V-s!USџ\ߙLu9Ұ^?˜Y6:o4K[c~)  z=Fs ˭X;pF4hx ˗F]06uʰZ#u8 {CCx4bkqt~hGZ8ȀMGCfǞry:qއz]+֑^mMd1/ׅ8~Vs@\!, bfjmm_۞ h_Ì bpPN?IƺF^Qr)cpGجVUzjqRMI;~łx>lRőղvl?`{݇׳=p߽S 4 mOhzhF\.'",~m .()*@O:p2fs#W 2.4U+ W͛ nTh6zļs4^@~8~#TSe\jU8&exnvnʎv BiT/,7OQe??#:/"ͱ_?M̙ӨFRܛ0iEf?Cj|g>)0O.D.!fI+(۔H_[ CwGSG?OqjCxhᒐ*11Qh-ϡJNΒ5Ola˵TNZIx[ RbBkݻNߗbQy*f}q2wQ[*b*(|,|Pvx|XMhy7J? `*(|ұVQ9߈nG ;O=V)(s2f FE<ۦ*o (Lei^AAA[ĩSS0Y>DAAA~̈f,Cg.b**((b%nе:TJؔ2RfjIENDB`console-slog-0.3.1/duration.go000066400000000000000000000050361455024307600163050ustar00rootroot00000000000000package console import "time" // appendDuration appends a string representing the duration in the form "72h3m0.5s". // Leading zero units are omitted. As a special case, durations less than one // second format use a smaller unit (milli-, micro-, or nanoseconds) to ensure // that the leading digit is non-zero. The zero duration formats as 0s. func appendDuration(dst []byte, d time.Duration) []byte { // Largest time is 2540400h10m10.000000000s var buf [32]byte w := len(buf) u := uint64(d) neg := d < 0 if neg { u = -u } if u < uint64(time.Second) { // Special case: if duration is smaller than a second, // use smaller units, like 1.2ms var prec int w-- buf[w] = 's' w-- switch { case u == 0: return append(dst, "0s"...) case u < uint64(time.Microsecond): // print nanoseconds prec = 0 buf[w] = 'n' case u < uint64(time.Millisecond): // print microseconds prec = 3 // U+00B5 'µ' micro sign == 0xC2 0xB5 w-- // Need room for two bytes. copy(buf[w:], "µ") default: // print milliseconds prec = 6 buf[w] = 'm' } w, u = fmtFrac(buf[:w], u, prec) w = fmtInt(buf[:w], u) } else { w-- buf[w] = 's' w, u = fmtFrac(buf[:w], u, 9) // u is now integer seconds w = fmtInt(buf[:w], u%60) u /= 60 // u is now integer minutes if u > 0 { w-- buf[w] = 'm' w = fmtInt(buf[:w], u%60) u /= 60 // u is now integer hours // Stop at hours because days can be different lengths. if u > 0 { w-- buf[w] = 'h' w = fmtInt(buf[:w], u%24) u /= 24 if u > 0 { w-- buf[w] = 'd' w = fmtInt(buf[:w], u) } } } } if neg { w-- buf[w] = '-' } return append(dst, buf[w:]...) } // fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the // tail of buf, omitting trailing zeros. It omits the decimal // point too when the fraction is 0. It returns the index where the // output bytes begin and the value v/10**prec. func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) { // Omit trailing zeros up to and including decimal point. w := len(buf) print := false for i := 0; i < prec; i++ { digit := v % 10 print = print || digit != 0 if print { w-- buf[w] = byte(digit) + '0' } v /= 10 } if print { w-- buf[w] = '.' } return w, v } // fmtInt formats v into the tail of buf. // It returns the index where the output begins. func fmtInt(buf []byte, v uint64) int { w := len(buf) if v == 0 { w-- buf[w] = '0' } else { for v > 0 { w-- buf[w] = byte(v%10) + '0' v /= 10 } } return w } console-slog-0.3.1/duration_test.go000066400000000000000000000024261455024307600173440ustar00rootroot00000000000000package console import ( "bytes" "testing" "time" ) func TestDuration(t *testing.T) { times := []time.Duration{ 2*time.Hour + 3*time.Minute + 4*time.Second + 5*time.Millisecond + 6*time.Microsecond + 7*time.Nanosecond, 3*time.Minute + 4*time.Second + 5*time.Millisecond + 6*time.Microsecond + 7*time.Nanosecond, 4*time.Second + 5*time.Millisecond + 6*time.Microsecond + 7*time.Nanosecond, 5*time.Millisecond + 6*time.Microsecond + 7*time.Nanosecond, 6*time.Microsecond + 7*time.Nanosecond, 7 * time.Nanosecond, time.Duration(0), 2*time.Hour + 7*time.Nanosecond, -2*time.Hour + 7*time.Nanosecond, } b := [4096]byte{} for _, tm := range times { bd := appendDuration(b[:0], tm) AssertEqual(t, tm.String(), string(bd)) } bd := appendDuration(b[:0], 49*time.Hour+1*time.Second) AssertEqual(t, "2d1h0m1s", string(bd)) } func BenchmarkDuration(b *testing.B) { d := 12*time.Hour + 13*time.Minute + 43*time.Second + 12*time.Millisecond b.Run("std", func(b *testing.B) { w := new(bytes.Buffer) w.Grow(2048) b.ResetTimer() for i := 0; i < b.N; i++ { w.WriteString(d.String()) w.Reset() } }) b.Run("append", func(b *testing.B) { w := new(buffer) w.Grow(2048) b.ResetTimer() for i := 0; i < b.N; i++ { w.AppendDuration(d) w.Reset() } }) } console-slog-0.3.1/encoding.go000066400000000000000000000105411455024307600162430ustar00rootroot00000000000000package console import ( "fmt" "log/slog" "path/filepath" "runtime" "time" ) type encoder struct { opts HandlerOptions } func (e encoder) NewLine(buf *buffer) { buf.AppendByte('\n') } func (e encoder) withColor(b *buffer, c ANSIMod, f func()) { if c == "" || e.opts.NoColor { f() return } b.AppendString(string(c)) f() b.AppendString(string(ResetMod)) } func (e encoder) writeColoredTime(w *buffer, t time.Time, format string, c ANSIMod) { e.withColor(w, c, func() { w.AppendTime(t, format) }) } func (e encoder) writeColoredString(w *buffer, s string, c ANSIMod) { e.withColor(w, c, func() { w.AppendString(s) }) } func (e encoder) writeColoredInt(w *buffer, i int64, c ANSIMod) { e.withColor(w, c, func() { w.AppendInt(i) }) } func (e encoder) writeColoredUint(w *buffer, i uint64, c ANSIMod) { e.withColor(w, c, func() { w.AppendUint(i) }) } func (e encoder) writeColoredFloat(w *buffer, i float64, c ANSIMod) { e.withColor(w, c, func() { w.AppendFloat(i) }) } func (e encoder) writeColoredBool(w *buffer, b bool, c ANSIMod) { e.withColor(w, c, func() { w.AppendBool(b) }) } func (e encoder) writeColoredDuration(w *buffer, d time.Duration, c ANSIMod) { e.withColor(w, c, func() { w.AppendDuration(d) }) } func (e encoder) writeTimestamp(buf *buffer, tt time.Time) { if !tt.IsZero() { e.writeColoredTime(buf, tt, e.opts.TimeFormat, e.opts.Theme.Timestamp()) buf.AppendByte(' ') } } func (e encoder) writeSource(buf *buffer, pc uintptr, cwd string) { frame, _ := runtime.CallersFrames([]uintptr{pc}).Next() if cwd != "" { if ff, err := filepath.Rel(cwd, frame.File); err == nil { frame.File = ff } } e.withColor(buf, e.opts.Theme.Source(), func() { buf.AppendString(frame.File) buf.AppendByte(':') buf.AppendInt(int64(frame.Line)) }) e.writeColoredString(buf, " > ", e.opts.Theme.AttrKey()) } func (e encoder) writeMessage(buf *buffer, level slog.Level, msg string) { if level >= slog.LevelInfo { e.writeColoredString(buf, msg, e.opts.Theme.Message()) } else { e.writeColoredString(buf, msg, e.opts.Theme.MessageDebug()) } } func (e encoder) writeAttr(buf *buffer, a slog.Attr, group string) { // Elide empty Attrs. if a.Equal(slog.Attr{}) { return } value := a.Value.Resolve() if value.Kind() == slog.KindGroup { subgroup := a.Key if group != "" { subgroup = group + "." + a.Key } for _, attr := range value.Group() { e.writeAttr(buf, attr, subgroup) } return } buf.AppendByte(' ') e.withColor(buf, e.opts.Theme.AttrKey(), func() { if group != "" { buf.AppendString(group) buf.AppendByte('.') } buf.AppendString(a.Key) buf.AppendByte('=') }) e.writeValue(buf, value) } func (e encoder) writeValue(buf *buffer, value slog.Value) { attrValue := e.opts.Theme.AttrValue() switch value.Kind() { case slog.KindInt64: e.writeColoredInt(buf, value.Int64(), attrValue) case slog.KindBool: e.writeColoredBool(buf, value.Bool(), attrValue) case slog.KindFloat64: e.writeColoredFloat(buf, value.Float64(), attrValue) case slog.KindTime: e.writeColoredTime(buf, value.Time(), e.opts.TimeFormat, attrValue) case slog.KindUint64: e.writeColoredUint(buf, value.Uint64(), attrValue) case slog.KindDuration: e.writeColoredDuration(buf, value.Duration(), attrValue) case slog.KindAny: switch v := value.Any().(type) { case error: e.writeColoredString(buf, v.Error(), e.opts.Theme.AttrValueError()) return case fmt.Stringer: e.writeColoredString(buf, v.String(), attrValue) return } fallthrough case slog.KindString: fallthrough default: e.writeColoredString(buf, value.String(), attrValue) } } func (e encoder) writeLevel(buf *buffer, l slog.Level) { var style ANSIMod var str string var delta int switch { case l >= slog.LevelError: style = e.opts.Theme.LevelError() str = "ERR" delta = int(l - slog.LevelError) case l >= slog.LevelWarn: style = e.opts.Theme.LevelWarn() str = "WRN" delta = int(l - slog.LevelWarn) case l >= slog.LevelInfo: style = e.opts.Theme.LevelInfo() str = "INF" delta = int(l - slog.LevelInfo) case l >= slog.LevelDebug: style = e.opts.Theme.LevelDebug() str = "DBG" delta = int(l - slog.LevelDebug) default: style = e.opts.Theme.LevelDebug() str = "DBG" delta = int(l - slog.LevelDebug) } if delta != 0 { str = fmt.Sprintf("%s%+d", str, delta) } e.writeColoredString(buf, str, style) buf.AppendByte(' ') } console-slog-0.3.1/example/000077500000000000000000000000001455024307600155605ustar00rootroot00000000000000console-slog-0.3.1/example/main.go000066400000000000000000000010371455024307600170340ustar00rootroot00000000000000package main import ( "errors" "log/slog" "os" "github.com/phsym/console-slog" ) func main() { logger := slog.New( console.NewHandler(os.Stderr, &console.HandlerOptions{Level: slog.LevelDebug, AddSource: true}), ) slog.SetDefault(logger) slog.Info("Hello world!", "foo", "bar") slog.Debug("Debug message") slog.Warn("Warning message") slog.Error("Error message", "err", errors.New("the error")) logger = logger.With("foo", "bar"). WithGroup("the-group"). With("bar", "baz") logger.Info("group info", "attr", "value") } console-slog-0.3.1/go.mod000066400000000000000000000000561455024307600152340ustar00rootroot00000000000000module github.com/phsym/console-slog go 1.21 console-slog-0.3.1/handler.go000066400000000000000000000060051455024307600160720ustar00rootroot00000000000000package console import ( "context" "io" "log/slog" "os" "strings" "sync" "time" ) var bufferPool = &sync.Pool{ New: func() any { return new(buffer) }, } var cwd, _ = os.Getwd() // HandlerOptions are options for a ConsoleHandler. // A zero HandlerOptions consists entirely of default values. type HandlerOptions struct { // AddSource causes the handler to compute the source code position // of the log statement and add a SourceKey attribute to the output. AddSource bool // Level reports the minimum record level that will be logged. // The handler discards records with lower levels. // If Level is nil, the handler assumes LevelInfo. // The handler calls Level.Level for each record processed; // to adjust the minimum level dynamically, use a LevelVar. Level slog.Leveler // Disable colorized output NoColor bool // TimeFormat is the format used for time.DateTime TimeFormat string // Theme defines the colorized output using ANSI escape sequences Theme Theme } type Handler struct { opts HandlerOptions out io.Writer group string context buffer enc *encoder } var _ slog.Handler = (*Handler)(nil) // NewHandler creates a Handler that writes to w, // using the given options. // If opts is nil, the default options are used. func NewHandler(out io.Writer, opts *HandlerOptions) *Handler { if opts == nil { opts = new(HandlerOptions) } if opts.Level == nil { opts.Level = slog.LevelInfo } if opts.TimeFormat == "" { opts.TimeFormat = time.DateTime } if opts.Theme == nil { opts.Theme = NewDefaultTheme() } return &Handler{ opts: *opts, // Copy struct out: out, group: "", context: nil, enc: &encoder{opts: *opts}, } } // Enabled implements slog.Handler. func (h *Handler) Enabled(_ context.Context, l slog.Level) bool { return l >= h.opts.Level.Level() } // Handle implements slog.Handler. func (h *Handler) Handle(_ context.Context, rec slog.Record) error { buf := bufferPool.Get().(*buffer) h.enc.writeTimestamp(buf, rec.Time) h.enc.writeLevel(buf, rec.Level) if h.opts.AddSource && rec.PC > 0 { h.enc.writeSource(buf, rec.PC, cwd) } h.enc.writeMessage(buf, rec.Level, rec.Message) buf.copy(&h.context) rec.Attrs(func(a slog.Attr) bool { h.enc.writeAttr(buf, a, h.group) return true }) h.enc.NewLine(buf) if _, err := buf.WriteTo(h.out); err != nil { buf.Reset() bufferPool.Put(buf) return err } bufferPool.Put(buf) return nil } // WithAttrs implements slog.Handler. func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { newCtx := h.context for _, a := range attrs { h.enc.writeAttr(&newCtx, a, h.group) } newCtx.Clip() return &Handler{ opts: h.opts, out: h.out, group: h.group, context: newCtx, enc: h.enc, } } // WithGroup implements slog.Handler. func (h *Handler) WithGroup(name string) slog.Handler { name = strings.TrimSpace(name) if h.group != "" { name = h.group + "." + name } return &Handler{ opts: h.opts, out: h.out, group: name, context: h.context, enc: h.enc, } } console-slog-0.3.1/handler_test.go000066400000000000000000000254241455024307600171370ustar00rootroot00000000000000package console import ( "bytes" "context" "errors" "fmt" "log/slog" "os" "path/filepath" "runtime" "testing" "time" ) func TestHandler_TimeFormat(t *testing.T) { buf := bytes.Buffer{} h := NewHandler(&buf, &HandlerOptions{TimeFormat: time.RFC3339Nano, NoColor: true}) now := time.Now() rec := slog.NewRecord(now, slog.LevelInfo, "foobar", 0) endTime := now.Add(time.Second) rec.AddAttrs(slog.Time("endtime", endTime)) AssertNoError(t, h.Handle(context.Background(), rec)) expected := fmt.Sprintf("%s INF foobar endtime=%s\n", now.Format(time.RFC3339Nano), endTime.Format(time.RFC3339Nano)) AssertEqual(t, expected, buf.String()) } // Handlers should not log the time field if it is zero. // '- If r.Time is the zero time, ignore the time.' // https://pkg.go.dev/log/slog@master#Handler func TestHandler_TimeZero(t *testing.T) { buf := bytes.Buffer{} h := NewHandler(&buf, &HandlerOptions{TimeFormat: time.RFC3339Nano, NoColor: true}) rec := slog.NewRecord(time.Time{}, slog.LevelInfo, "foobar", 0) AssertNoError(t, h.Handle(context.Background(), rec)) expected := fmt.Sprintf("INF foobar\n") AssertEqual(t, expected, buf.String()) } func TestHandler_NoColor(t *testing.T) { buf := bytes.Buffer{} h := NewHandler(&buf, &HandlerOptions{NoColor: true}) now := time.Now() rec := slog.NewRecord(now, slog.LevelInfo, "foobar", 0) AssertNoError(t, h.Handle(context.Background(), rec)) expected := fmt.Sprintf("%s INF foobar\n", now.Format(time.DateTime)) AssertEqual(t, expected, buf.String()) } type theStringer struct{} func (t theStringer) String() string { return "stringer" } type noStringer struct { Foo string } func TestHandler_Attr(t *testing.T) { buf := bytes.Buffer{} h := NewHandler(&buf, &HandlerOptions{NoColor: true}) now := time.Now() rec := slog.NewRecord(now, slog.LevelInfo, "foobar", 0) rec.AddAttrs( slog.Bool("bool", true), slog.Int("int", -12), slog.Uint64("uint", 12), slog.Float64("float", 3.14), slog.String("foo", "bar"), slog.Time("time", now), slog.Duration("dur", time.Second), slog.Group("group", slog.String("foo", "bar"), slog.Group("subgroup", slog.String("foo", "bar"))), slog.Any("err", errors.New("the error")), slog.Any("stringer", theStringer{}), slog.Any("nostringer", noStringer{Foo: "bar"}), slog.Attr{}, slog.Any("", nil), ) AssertNoError(t, h.Handle(context.Background(), rec)) expected := fmt.Sprintf("%s INF foobar bool=true int=-12 uint=12 float=3.14 foo=bar time=%s dur=1s group.foo=bar group.subgroup.foo=bar err=the error stringer=stringer nostringer={bar}\n", now.Format(time.DateTime), now.Format(time.DateTime)) AssertEqual(t, expected, buf.String()) } func TestHandler_WithAttr(t *testing.T) { buf := bytes.Buffer{} h := NewHandler(&buf, &HandlerOptions{NoColor: true}) now := time.Now() rec := slog.NewRecord(now, slog.LevelInfo, "foobar", 0) h2 := h.WithAttrs([]slog.Attr{ slog.Bool("bool", true), slog.Int("int", -12), slog.Uint64("uint", 12), slog.Float64("float", 3.14), slog.String("foo", "bar"), slog.Time("time", now), slog.Duration("dur", time.Second), slog.Group("group", slog.String("foo", "bar"), slog.Group("subgroup", slog.String("foo", "bar"))), }) AssertNoError(t, h2.Handle(context.Background(), rec)) expected := fmt.Sprintf("%s INF foobar bool=true int=-12 uint=12 float=3.14 foo=bar time=%s dur=1s group.foo=bar group.subgroup.foo=bar\n", now.Format(time.DateTime), now.Format(time.DateTime)) AssertEqual(t, expected, buf.String()) buf.Reset() AssertNoError(t, h.Handle(context.Background(), rec)) AssertEqual(t, fmt.Sprintf("%s INF foobar\n", now.Format(time.DateTime)), buf.String()) } func TestHandler_WithGroup(t *testing.T) { buf := bytes.Buffer{} h := NewHandler(&buf, &HandlerOptions{NoColor: true}) now := time.Now() rec := slog.NewRecord(now, slog.LevelInfo, "foobar", 0) rec.Add("int", 12) h2 := h.WithGroup("group1").WithAttrs([]slog.Attr{slog.String("foo", "bar")}) AssertNoError(t, h2.Handle(context.Background(), rec)) expected := fmt.Sprintf("%s INF foobar group1.foo=bar group1.int=12\n", now.Format(time.DateTime)) AssertEqual(t, expected, buf.String()) buf.Reset() h3 := h2.WithGroup("group2") AssertNoError(t, h3.Handle(context.Background(), rec)) expected = fmt.Sprintf("%s INF foobar group1.foo=bar group1.group2.int=12\n", now.Format(time.DateTime)) AssertEqual(t, expected, buf.String()) buf.Reset() AssertNoError(t, h.Handle(context.Background(), rec)) AssertEqual(t, fmt.Sprintf("%s INF foobar int=12\n", now.Format(time.DateTime)), buf.String()) } func TestHandler_Levels(t *testing.T) { levels := map[slog.Level]string{ slog.LevelDebug - 1: "DBG-1", slog.LevelDebug: "DBG", slog.LevelDebug + 1: "DBG+1", slog.LevelInfo: "INF", slog.LevelInfo + 1: "INF+1", slog.LevelWarn: "WRN", slog.LevelWarn + 1: "WRN+1", slog.LevelError: "ERR", slog.LevelError + 1: "ERR+1", } for l := range levels { t.Run(l.String(), func(t *testing.T) { buf := bytes.Buffer{} h := NewHandler(&buf, &HandlerOptions{Level: l, NoColor: true}) for ll, s := range levels { AssertEqual(t, ll >= l, h.Enabled(context.Background(), ll)) now := time.Now() rec := slog.NewRecord(now, ll, "foobar", 0) if ll >= l { AssertNoError(t, h.Handle(context.Background(), rec)) AssertEqual(t, fmt.Sprintf("%s %s foobar\n", now.Format(time.DateTime), s), buf.String()) buf.Reset() } } }) } } func TestHandler_Source(t *testing.T) { buf := bytes.Buffer{} h := NewHandler(&buf, &HandlerOptions{NoColor: true, AddSource: true}) h2 := NewHandler(&buf, &HandlerOptions{NoColor: true, AddSource: false}) pc, file, line, _ := runtime.Caller(0) now := time.Now() rec := slog.NewRecord(now, slog.LevelInfo, "foobar", pc) AssertNoError(t, h.Handle(context.Background(), rec)) cwd, _ := os.Getwd() file, _ = filepath.Rel(cwd, file) AssertEqual(t, fmt.Sprintf("%s INF %s:%d > foobar\n", now.Format(time.DateTime), file, line), buf.String()) buf.Reset() AssertNoError(t, h2.Handle(context.Background(), rec)) AssertEqual(t, fmt.Sprintf("%s INF foobar\n", now.Format(time.DateTime)), buf.String()) } func TestHandler_Err(t *testing.T) { w := writerFunc(func(b []byte) (int, error) { return 0, errors.New("nope") }) h := NewHandler(w, &HandlerOptions{NoColor: true}) rec := slog.NewRecord(time.Now(), slog.LevelInfo, "foobar", 0) AssertError(t, h.Handle(context.Background(), rec)) } func TestThemes(t *testing.T) { for _, theme := range []Theme{ NewDefaultTheme(), NewBrightTheme(), } { t.Run(theme.Name(), func(t *testing.T) { level := slog.LevelInfo rec := slog.Record{} buf := bytes.Buffer{} bufBytes := buf.Bytes() now := time.Now() timeFormat := time.Kitchen index := -1 toIndex := -1 h := NewHandler(&buf, &HandlerOptions{ AddSource: true, TimeFormat: timeFormat, Theme: theme, }).WithAttrs([]slog.Attr{{Key: "pid", Value: slog.IntValue(37556)}}) var pcs [1]uintptr runtime.Callers(1, pcs[:]) checkANSIMod := func(t *testing.T, name string, ansiMod ANSIMod) { t.Run(name, func(t *testing.T) { index = bytes.IndexByte(bufBytes, '\x1b') AssertNotEqual(t, -1, index) toIndex = index + len(ansiMod) AssertEqual(t, ansiMod, ANSIMod(bufBytes[index:toIndex])) bufBytes = bufBytes[toIndex:] index = bytes.IndexByte(bufBytes, '\x1b') AssertNotEqual(t, -1, index) toIndex = index + len(ResetMod) AssertEqual(t, ResetMod, ANSIMod(bufBytes[index:toIndex])) bufBytes = bufBytes[toIndex:] }) } checkLog := func(level slog.Level, attrCount int) { t.Run("CheckLog_"+level.String(), func(t *testing.T) { println("log: ", string(buf.Bytes())) // Timestamp if theme.Timestamp() != "" { checkANSIMod(t, "Timestamp", theme.Timestamp()) } // Level if theme.Level(level) != "" { checkANSIMod(t, level.String(), theme.Level(level)) } // Source if theme.Source() != "" { checkANSIMod(t, "Source", theme.Source()) checkANSIMod(t, "AttrKey", theme.AttrKey()) } // Message if level >= slog.LevelInfo { if theme.Message() != "" { checkANSIMod(t, "Message", theme.Message()) } } else { if theme.MessageDebug() != "" { checkANSIMod(t, "MessageDebug", theme.MessageDebug()) } } for i := 0; i < attrCount; i++ { // AttrKey if theme.AttrKey() != "" { checkANSIMod(t, "AttrKey", theme.AttrKey()) } // AttrValue if theme.AttrValue() != "" { checkANSIMod(t, "AttrValue", theme.AttrValue()) } } }) } buf.Reset() level = slog.LevelDebug - 1 rec = slog.NewRecord(now, level, "Access", pcs[0]) rec.Add("database", "myapp", "host", "localhost:4962") h.Handle(context.Background(), rec) bufBytes = buf.Bytes() checkLog(level, 3) buf.Reset() level = slog.LevelDebug rec = slog.NewRecord(now, level, "Access", pcs[0]) rec.Add("database", "myapp", "host", "localhost:4962") h.Handle(context.Background(), rec) bufBytes = buf.Bytes() checkLog(level, 3) buf.Reset() level = slog.LevelDebug + 1 rec = slog.NewRecord(now, level, "Access", pcs[0]) rec.Add("database", "myapp", "host", "localhost:4962") h.Handle(context.Background(), rec) bufBytes = buf.Bytes() checkLog(level, 3) buf.Reset() level = slog.LevelInfo rec = slog.NewRecord(now, level, "Starting listener", pcs[0]) rec.Add("listen", ":8080") h.Handle(context.Background(), rec) bufBytes = buf.Bytes() checkLog(level, 2) buf.Reset() level = slog.LevelInfo + 1 rec = slog.NewRecord(now, level, "Access", pcs[0]) rec.Add("method", "GET", "path", "/users", "resp_time", time.Millisecond*10) h.Handle(context.Background(), rec) bufBytes = buf.Bytes() checkLog(level, 4) buf.Reset() level = slog.LevelWarn rec = slog.NewRecord(now, level, "Slow request", pcs[0]) rec.Add("method", "POST", "path", "/posts", "resp_time", time.Second*532) h.Handle(context.Background(), rec) bufBytes = buf.Bytes() checkLog(level, 4) buf.Reset() level = slog.LevelWarn + 1 rec = slog.NewRecord(now, level, "Slow request", pcs[0]) rec.Add("method", "POST", "path", "/posts", "resp_time", time.Second*532) h.Handle(context.Background(), rec) bufBytes = buf.Bytes() checkLog(level, 4) buf.Reset() level = slog.LevelError rec = slog.NewRecord(now, level, "Database connection lost", pcs[0]) rec.Add("database", "myapp", "error", errors.New("connection reset by peer")) h.Handle(context.Background(), rec) bufBytes = buf.Bytes() checkLog(level, 3) buf.Reset() level = slog.LevelError + 1 rec = slog.NewRecord(now, level, "Database connection lost", pcs[0]) rec.Add("database", "myapp", "error", errors.New("connection reset by peer")) h.Handle(context.Background(), rec) bufBytes = buf.Bytes() checkLog(level, 3) }) } } console-slog-0.3.1/theme.go000066400000000000000000000064061455024307600155640ustar00rootroot00000000000000package console import ( "fmt" "log/slog" ) type ANSIMod string var ResetMod = ToANSICode(Reset) const ( Reset = iota Bold Faint Italic Underline CrossedOut = 9 ) const ( Black = iota + 30 Red Green Yellow Blue Magenta Cyan Gray ) const ( BrightBlack = iota + 90 BrightRed BrightGreen BrightYellow BrightBlue BrightMagenta BrightCyan White ) func (c ANSIMod) String() string { return string(c) } func ToANSICode(modes ...int) ANSIMod { if len(modes) == 0 { return "" } var s string for i, m := range modes { if i > 0 { s += ";" } s += fmt.Sprintf("%d", m) } return ANSIMod("\x1b[" + s + "m") } type Theme interface { Name() string Timestamp() ANSIMod Source() ANSIMod Message() ANSIMod MessageDebug() ANSIMod AttrKey() ANSIMod AttrValue() ANSIMod AttrValueError() ANSIMod LevelError() ANSIMod LevelWarn() ANSIMod LevelInfo() ANSIMod LevelDebug() ANSIMod Level(level slog.Level) ANSIMod } type ThemeDef struct { name string timestamp ANSIMod source ANSIMod message ANSIMod messageDebug ANSIMod attrKey ANSIMod attrValue ANSIMod attrValueError ANSIMod levelError ANSIMod levelWarn ANSIMod levelInfo ANSIMod levelDebug ANSIMod } func (t ThemeDef) Name() string { return t.name } func (t ThemeDef) Timestamp() ANSIMod { return t.timestamp } func (t ThemeDef) Source() ANSIMod { return t.source } func (t ThemeDef) Message() ANSIMod { return t.message } func (t ThemeDef) MessageDebug() ANSIMod { return t.messageDebug } func (t ThemeDef) AttrKey() ANSIMod { return t.attrKey } func (t ThemeDef) AttrValue() ANSIMod { return t.attrValue } func (t ThemeDef) AttrValueError() ANSIMod { return t.attrValueError } func (t ThemeDef) LevelError() ANSIMod { return t.levelError } func (t ThemeDef) LevelWarn() ANSIMod { return t.levelWarn } func (t ThemeDef) LevelInfo() ANSIMod { return t.levelInfo } func (t ThemeDef) LevelDebug() ANSIMod { return t.levelDebug } func (t ThemeDef) Level(level slog.Level) ANSIMod { switch { case level >= slog.LevelError: return t.LevelError() case level >= slog.LevelWarn: return t.LevelWarn() case level >= slog.LevelInfo: return t.LevelInfo() default: return t.LevelDebug() } } func NewDefaultTheme() Theme { return ThemeDef{ name: "Default", timestamp: ToANSICode(BrightBlack), source: ToANSICode(Bold, BrightBlack), message: ToANSICode(Bold), messageDebug: ToANSICode(), attrKey: ToANSICode(Cyan), attrValue: ToANSICode(), attrValueError: ToANSICode(Bold, Red), levelError: ToANSICode(Red), levelWarn: ToANSICode(Yellow), levelInfo: ToANSICode(Green), levelDebug: ToANSICode(), } } func NewBrightTheme() Theme { return ThemeDef{ name: "Bright", timestamp: ToANSICode(Gray), source: ToANSICode(Bold, Gray), message: ToANSICode(Bold, White), messageDebug: ToANSICode(), attrKey: ToANSICode(BrightCyan), attrValue: ToANSICode(), attrValueError: ToANSICode(Bold, BrightRed), levelError: ToANSICode(BrightRed), levelWarn: ToANSICode(BrightYellow), levelInfo: ToANSICode(BrightGreen), levelDebug: ToANSICode(), } } console-slog-0.3.1/utils_test.go000066400000000000000000000022621455024307600166550ustar00rootroot00000000000000package console import ( "cmp" "testing" ) func AssertZero[E comparable](t *testing.T, v E) { t.Helper() var zero E if v != zero { t.Errorf("expected zero value, got %v", v) } } func AssertEqual[E comparable](t *testing.T, expected, value E) { t.Helper() if expected != value { t.Errorf("expected %v, got %v", expected, value) } } func AssertNotEqual[E comparable](t *testing.T, expected, value E) { t.Helper() if expected == value { t.Errorf("expected to be different, got %v", value) } } func AssertGreaterOrEqual[E cmp.Ordered](t *testing.T, expected, value E) { t.Helper() if expected > value { t.Errorf("expected to be %v to be greater than %v", value, expected) } } func AssertNoError(t *testing.T, err error) { t.Helper() if err != nil { t.Errorf("expected no error, got %q", err.Error()) } } func AssertError(t *testing.T, err error) { t.Helper() if err == nil { t.Error("expected an error, got nil") } } // func AssertNil(t *testing.T, value any) { // t.Helper() // if value != nil { // t.Errorf("expected nil, got %v", value) // } // } type writerFunc func([]byte) (int, error) func (w writerFunc) Write(b []byte) (int, error) { return w(b) }