pax_global_header00006660000000000000000000000064150432077520014517gustar00rootroot0000000000000052 comment=f2e807765f3fc883653a4e1dafb94821cec6e404 golang-github-olekukonko-ll-0.0.9/000077500000000000000000000000001504320775200170405ustar00rootroot00000000000000golang-github-olekukonko-ll-0.0.9/.gitignore000066400000000000000000000000311504320775200210220ustar00rootroot00000000000000.idea lab tmp #_* _test/ golang-github-olekukonko-ll-0.0.9/LICENSE000066400000000000000000000020541504320775200200460ustar00rootroot00000000000000MIT License Copyright (c) 2025 Oleku Konko 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. golang-github-olekukonko-ll-0.0.9/README.md000066400000000000000000000310731504320775200203230ustar00rootroot00000000000000# ll - A Modern Structured Logging Library for Go `ll` is a high-performance, production-ready logging library for Go, designed to provide **hierarchical namespaces**, **structured logging**, **middleware pipelines**, **conditional logging**, and support for multiple output formats, including text, JSON, colorized logs, and compatibility with Go’s `slog`. It’s ideal for applications requiring fine-grained log control, extensibility, and scalability. ## Key Features - **Hierarchical Namespaces**: Organize logs with fine-grained control over subsystems (e.g., "app/db"). - **Structured Logging**: Add key-value metadata for machine-readable logs. - **Middleware Pipeline**: Customize log processing with error-based rejection. - **Conditional Logging**: Optimize performance by skipping unnecessary log operations. - **Multiple Output Formats**: Support for text, JSON, colorized logs, and `slog` integration. - **Debugging Utilities**: Inspect variables (`Dbg`), binary data (`Dump`), and stack traces (`Stack`). - **Thread-Safe**: Built for concurrent use with mutex-protected state. - **Performance Optimized**: Minimal allocations and efficient namespace caching. ## Installation Install `ll` using Go modules: ```bash go get github.com/olekukonko/ll ``` Ensure you have Go 1.21 or later for optimal compatibility. ## Getting Started Here’s a quick example to start logging with `ll`: ```go package main import ( "github.com/olekukonko/ll" ) func main() { // Create a logger with namespace "app" logger := ll.New("") // enable output logger.Enable() // Basic log logger.Info("Welcome") // Output: [app] INFO: Application started logger = logger.Namespace("app") // Basic log logger.Info("start at :8080") // Output: [app] INFO: Application started //Output //INFO: Welcome //[app] INFO: start at :8080 } ``` ```go package main import ( "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "os" ) func main() { // Chaining logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout)) // Basic log logger.Info("Application started") // Output: [app] INFO: Application started // Structured log with fields logger.Fields("user", "alice", "status", 200).Info("User logged in") // Output: [app] INFO: User logged in [user=alice status=200] // Conditional log debugMode := false logger.If(debugMode).Debug("Debug info") // No output (debugMode is false) } ``` ## Core Features ### 1. Hierarchical Namespaces Namespaces allow you to organize logs hierarchically, enabling precise control over logging for different parts of your application. This is especially useful for large systems with multiple components. **Benefits**: - **Granular Control**: Enable/disable logs for specific subsystems (e.g., "app/db" vs. "app/api"). - **Scalability**: Manage log volume in complex applications. - **Readability**: Clear namespace paths improve traceability. **Example**: ```go logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout)) // Child loggers dbLogger := logger.Namespace("db") apiLogger := logger.Namespace("api").Style(lx.NestedPath) // Namespace control logger.NamespaceEnable("app/db") // Enable DB logs logger.NamespaceDisable("app/api") // Disable API logs dbLogger.Info("Query executed") // Output: [app/db] INFO: Query executed apiLogger.Info("Request received") // No output ``` ### 2. Structured Logging Add key-value metadata to logs for machine-readable output, making it easier to query and analyze logs in tools like ELK or Grafana. **Example**: ```go logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout)) // Variadic fields logger.Fields("user", "bob", "status", 200).Info("Request completed") // Output: [app] INFO: Request completed [user=bob status=200] // Map-based fields logger.Field(map[string]interface{}{"method": "GET"}).Info("Request") // Output: [app] INFO: Request [method=GET] ``` ### 3. Middleware Pipeline Customize log processing with a middleware pipeline. Middleware functions can enrich, filter, or transform logs, using an error-based rejection mechanism (non-nil errors stop logging). **Example**: ```go logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout)) // Enrich logs with app metadata logger.Use(ll.FuncMiddleware(func(e *lx.Entry) error { if e.Fields == nil { e.Fields = make(map[string]interface{}) } e.Fields["app"] = "myapp" return nil })) // Filter low-level logs logger.Use(ll.FuncMiddleware(func(e *lx.Entry) error { if e.Level < lx.LevelWarn { return fmt.Errorf("level too low") } return nil })) logger.Info("Ignored") // No output (filtered) logger.Warn("Warning") // Output: [app] WARN: Warning [app=myapp] ``` ### 4. Conditional Logging Optimize performance by skipping expensive log operations when conditions are false, ideal for production environments. **Example**: ```go logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout)) featureEnabled := true logger.If(featureEnabled).Fields("action", "update").Info("Feature used") // Output: [app] INFO: Feature used [action=update] logger.If(false).Info("Ignored") // No output, no processing ``` ### 5. Multiple Output Formats `ll` supports various output formats, including human-readable text, colorized logs, JSON, and integration with Go’s `slog` package. **Example**: ```go logger := ll.New("app").Enable() // Text output logger.Handler(lh.NewTextHandler(os.Stdout)) logger.Info("Text log") // Output: [app] INFO: Text log // JSON output logger.Handler(lh.NewJSONHandler(os.Stdout, time.RFC3339Nano)) logger.Info("JSON log") // Output: {"timestamp":"...","level":"INFO","message":"JSON log","namespace":"app"} // Slog integration slogText := slog.NewTextHandler(os.Stdout, nil) logger.Handler(lh.NewSlogHandler(slogText)) logger.Info("Slog log") // Output: level=INFO msg="Slog log" namespace=app class=Text ``` ### 6. Debugging Utilities `ll` provides powerful tools for debugging, including variable inspection, binary data dumps, and stack traces. #### Core Debugging Methods 1. **Dbg - Contextual Inspection** Inspects variables with file and line context, preserving variable names and handling all Go types. ```go x := 42 user := struct{ Name string }{"Alice"} ll.Dbg(x) // Output: [file.go:123] x = 42 ll.Dbg(user) // Output: [file.go:124] user = [Name:Alice] ``` 2. **Dump - Binary Inspection** Displays a hex/ASCII view of data, optimized for strings, bytes, and complex types (with JSON fallback). ```go ll.Handler(lh.NewColorizedHandler(os.Stdout)) ll.Dump("hello\nworld") // Output: Hex/ASCII dump (see example/dump.png) ``` 3. **Stack - Stack Inspection** Logs a stack trace for debugging critical errors. ```go ll.Handler(lh.NewColorizedHandler(os.Stdout)) ll.Stack("Critical error") // Output: [app] ERROR: Critical error [stack=...] (see example/stack.png) ``` #### Performance Tracking Measure execution time for performance analysis. ```go // Automatic measurement defer ll.Measure(func() { time.Sleep(time.Millisecond) })() // Output: [app] INFO: function executed [duration=~1ms] // Explicit benchmarking start := time.Now() time.Sleep(time.Millisecond) ll.Benchmark(start) // Output: [app] INFO: benchmark [start=... end=... duration=...] ``` **Performance Notes**: - `Dbg` calls are disabled at compile-time when not enabled. - `Dump` optimizes for primitive types, strings, and bytes with zero-copy paths. - Stack traces are configurable via `StackSize`. ## Real-World Example: Web Server A practical example of using `ll` in a web server with structured logging, middleware, and `slog` integration: ```go package main import ( "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "log/slog" "net/http" "os" "time" ) func main() { // Initialize logger with slog handler slogHandler := slog.NewJSONHandler(os.Stdout, nil) logger := ll.New("server").Enable().Handler(lh.NewSlogHandler(slogHandler)) // HTTP child logger httpLogger := logger.Namespace("http").Style(lx.NestedPath) // Middleware for request ID httpLogger.Use(ll.FuncMiddleware(func(e *lx.Entry) error { if e.Fields == nil { e.Fields = make(map[string]interface{}) } e.Fields["request_id"] = "req-" + time.Now().String() return nil })) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { start := time.Now() httpLogger.Fields("method", r.Method, "path", r.URL.Path).Info("Request received") w.Write([]byte("Hello, world!")) httpLogger.Fields("duration_ms", time.Since(start).Milliseconds()).Info("Request completed") }) logger.Info("Starting server on :8080") http.ListenAndServe(":8080", nil) } ``` **Sample Output (JSON via slog)**: ```json {"level":"INFO","msg":"Starting server on :8080","namespace":"server"} {"level":"INFO","msg":"Request received","namespace":"server/http","class":"Text","method":"GET","path":"/","request_id":"req-..."} {"level":"INFO","msg":"Request completed","namespace":"server/http","class":"Text","duration_ms":1,"request_id":"req-..."} ``` ## Why Choose `ll`? - **Granular Control**: Hierarchical namespaces for precise log management. - **Performance**: Conditional logging and optimized concatenation reduce overhead. - **Extensibility**: Middleware pipeline for custom log processing. - **Structured Output**: Machine-readable logs with key-value metadata. - **Flexible Formats**: Text, JSON, colorized, and `slog` support. - **Debugging Power**: Advanced tools like `Dbg`, `Dump`, and `Stack` for deep inspection. - **Thread-Safe**: Safe for concurrent use in high-throughput applications. ## Comparison with Other Libraries | Feature | `ll` | `log` (stdlib) | `slog` (stdlib) | `zap` | |--------------------------|--------------------------|----------------|-----------------|-------------------| | Hierarchical Namespaces | ✅ | ❌ | ❌ | ❌ | | Structured Logging | ✅ (Fields, Context) | ❌ | ✅ | ✅ | | Middleware Pipeline | ✅ | ❌ | ❌ | ✅ (limited) | | Conditional Logging | ✅ (If, IfOne, IfAny) | ❌ | ❌ | ❌ | | Slog Compatibility | ✅ | ❌ | ✅ (native) | ❌ | | Debugging (Dbg, Dump) | ✅ | ❌ | ❌ | ❌ | | Performance (disabled logs) | High (conditional) | Low | Medium | High | | Output Formats | Text, JSON, Color, Slog | Text | Text, JSON | JSON, Text | ## Benchmarks `ll` is optimized for performance, particularly for disabled logs and structured logging: - **Disabled Logs**: 30% faster than `slog` due to efficient conditional checks. - **Structured Logging**: 2x faster than `log` with minimal allocations. - **Namespace Caching**: Reduces overhead for hierarchical lookups. See `ll_bench_test.go` for detailed benchmarks on namespace creation, cloning, and field building. ## Testing and Stability The `ll` library includes a comprehensive test suite (`ll_test.go`) covering: - Logger configuration, namespaces, and conditional logging. - Middleware, rate limiting, and sampling. - Handler output formats (text, JSON, slog). - Debugging utilities (`Dbg`, `Dump`, `Stack`). Recent improvements: - Fixed sampling middleware for reliable behavior at edge cases (0.0 and 1.0 rates). - Enhanced documentation across `conditional.go`, `field.go`, `global.go`, `ll.go`, `lx.go`, and `ns.go`. - Added `slog` compatibility via `lh.SlogHandler`. ## Contributing Contributions are welcome! To contribute: 1. Fork the repository: `github.com/olekukonko/ll`. 2. Create a feature branch: `git checkout -b feature/your-feature`. 3. Commit changes: `git commit -m "Add your feature"`. 4. Push to the branch: `git push origin feature/your-feature`. 5. Open a pull request with a clear description. Please include tests in `ll_test.go` and update documentation as needed. Follow the Go coding style and run `go test ./...` before submitting. ## License `ll` is licensed under the MIT License. See [LICENSE](LICENSE) for details. ## Resources - **Source Code**: [github.com/olekukonko/ll](https://github.com/olekukonko/ll) - **Issue Tracker**: [github.com/olekukonko/ll/issues](https://github.com/olekukonko/ll/issues) - **GoDoc**: [pkg.go.dev/github.com/olekukonko/ll](https://pkg.go.dev/github.com/olekukonko/ll)golang-github-olekukonko-ll-0.0.9/_example/000077500000000000000000000000001504320775200206325ustar00rootroot00000000000000golang-github-olekukonko-ll-0.0.9/_example/dump.png000066400000000000000000003444361504320775200223230ustar00rootroot00000000000000PNG  IHDR^O KiCCPICC ProfileHWXS[R!D@JM@J-"JHcBP ]D aWZ *+b tW77wϙ9w;Ri @$O"uh0_ r,˻QZEK( @ NoHey7'UȠW)q 7)q _鳉B:/@|AԡhD(@Ond!s!6pNRN45Ari]rssXê) Q ${rCA 6(.+13SGmr.`BV. ߙ+;)Eyq!\aO>a,XH‰Dž"b"I|y14'+y3cTEҼ8xy?4J.,5 LY@UT=Ad C?30"G @>axS]@zR%<8xS z@F ` 9*=?~g8 g3@b1D p W?Xq61w{SBpgP6˱'VPǽ:Tƙp]< Ynʬh-PʼnRQ(6CGji(sc~T ;3t~6l%;Nb&XւUo O6f?Ye&N5NN_T}yiyȝ,.gd8!b$,g'gWMtwa|#߹s96p@!WqBo:}3p^P @τ\`(%`9X&T`?M$8 .+ WOx;ABC>bX"3F|@$AT$ d&2)AV"هFN"6D^#P UGuP# Qơ t Z.@eh% COh;11Scc\,K16+JJk֎uaq"Y\!x<.%x9^kCF O0AJ("AxG$Dk;܋I,  b1D"IޤHG*"#"']%u>&dgr9,!K;Wȟ)K'%"L,l4R.S:(ZTk75EG-RPQߨyE檕U;P:W=E]T} ;oh4͏Lˣ-UN>h045xB9uW5^)tK:>^@/_wiR44|ٚ5oih1FiEjj-کuA6IJ;P[@{) a2m3O'KDgNNn4 ݣLi1s˘7  [[ W ;Eޢg+ӟgxg,sWY!YgGfIٓKM=,іdKNO6^5vUjoLZsԥtZZu떯RY~¿bz׿ puMFJ6},|{KJҭĭ[nKv/ lC*t{uNÝjEM箔]Wvnuݲd/ثǾ}7o>>P{CCuHáq<ɴeǨ=^pDɌ'5=5ѧ[τ96ss{oyEKnZ\[Vֺx\ilvՓλ~Fč7oJ~[x;~ݹk/}`w}Q죻_w6/WKWW枨r}~_ACGs?=< KWۯ¾e_ (6- <7Rǫ·}QiOXu+n.ws hyt:p;w*  6GMMΤ?=JU0w߃?SbeXIfMM*>F(iNx^ASCIIScreenshottv pHYs%%IR$iTXtXML:com.adobe.xmp 482 862 Screenshot ',iDOT(赻@IDATx]`@zHGPAP EJGzBJWAR4Jk 콻{y-!@9 vvfݽi- ,e+^2Ǐѹ?')ӥժxbO(ݿu)A@A@ǀt z JA@A@A@xvԵTA@A@BVA@A@x=;u-%A@A@DŽA@A@g!^N]KIA@A@A1! 1/jA@A@AA@׳SRRA@A@A@xLzLZA@A@A@xv:u̔!o^ v<-eʒ;|bզRQ…)}?$j T(v~qN*&  HRīFT\/K4H7·Ҿ=c/[Ps)y:n/Y0QOfKcQQԩP3o'TnǑ7Ҝ/$lŊ ! ǎҦ%Biq"пz]; ~q"ER>}'&bΒ#Чwojּ9$b}FK,EE;=00f͞F/׻Oojb QpFg%nq <ONi-o-]^Md~ɵ)rQ ī›,Yr@`FBБ%6"^F/,8]'-[X)#:J>=T郲!x1aGtpGdk5=7z^I] 6{vjk*\.H5\4v$-֕ݾ- :u*U^.?GAAqbMߨ9sPRh{w!0Qh֬-IR$ONϧs|wXwehРE0w5v"(9Nт &]'6M4xCn.rR)[lty:yC7Q{I ֽsKM^\~;\U94mzy'\1P652ZM8YK.#ŝY~EZjmh^l ;1LJ7ot+Kυ'^˧W+]$߶ρ%ut̘12{Gz>y|טGIRA@ɇ,%[yJ.?EEߣ۷{W(**^%WbxeZ8F:{v7>YO,Hr-G|RȜ/۷z;7Y2J-;Ln]Hר@D a/{\|ttm: si~if+PQ:Ew^Ȼtw:SedD^Ly8†{7n8Ϗbv@t%wQY4iJ]Ze3Eݻ4żPwJY3h;Ix,ϗFvA΀4zT}}=gs X5iQ\)/"oES(( ΣFO鋤"t~t߻tx8D&nH^1cFe)a2;aǚijgұc袭~y}H}7\T\2eJz{?6Njѯ][Tɒte:x^q+{___ʐ!EFF-qJ) c?4͑#-[vIu+W\"E y&ݼqk3c>[3t:ryn߾x]:I=;{΋&…ꫯzOrq)_<̙.{|r5_F!,Eg[@@g׮8 hf6ڴ.S(c?r(RЯǕPÆi޼IOWz^@׻k~/5Y#vza [~j' R h2Zcg}`|km^ FҥπC>+hʔ)؝'FPZoa}SAA=ڨާ7jC #RZ'Ǒ;}sIܭq3"Uԝ_xx'_=]o2eTfNxxʑ@ykڶW,nu+r'҆m/#_C4vMEe0,'تUf-P6}2x)ݽvƕ-eηH4uĀQJ,V^fwʊE*˛FvhgX0U;dHo,^F%o/~}Vqǵz6f(1Q4,onsӴ 84jd=.6ns+׷/ڡ=q OqAܧk(Ai֬YYuT TQ~ٿ B?M~ TG5ijv=v:x3rFxGt ~G\l(4Q`}lgϞ=R/z Y-q˹_KѻwC,jxϳڵQZR-"ϳh3?;˞twN1cP 5ATu6`%F/dȶóIU<9RV-@HC@ 4B_SnwakIz '֮\yjS6]1Qty*;'W6OZq( /W =\CK!?#771]F9O~xɏ7:Ёe M.6lH`e,|}0~˙|=+r>(PM{1x DQ0<<דVt뾖| 0Dn;_gi򦤺i(*Ծ@ЙNmJ+)Oӧe+VZI&/nQ*T@w|/sIwޣ.;)xg2ZB0`<u.R<̙3-G O@Ǐ5kV*ٞOg|J~ɒ-xq UxqYg!^Y]Վ| b9s-[zA)tze pX<:u]FkC4=zDôXaxl6mZ;'zЍP0 x uldZ&-cP  .d]ah3>i{ CΝ;4x4LZ ;T_^]+VTX'G,^[???%ۮ];?یpv1ܮ#;iXdžk^8я/6oۏ֢!#vMի53gxMʔ)\w2~79F/׋L ?k9:zwe~ai*F۶mg+W:z7r.ΐ!N:d'*F{RuiU߽k*bwL)-\O?ݰqp"m!-\P}z̒9B;?0??j7lTS]Gc{?TC^ n>Ap" h_-Urk=ֵSk]&_CtxK'^TPe_ ,<5mMuOUA:8t|FV⥇g9]_v.Pޫݳq 1vy; NlA8ͺlަMqniZUXz&;R)uY&mxȒK5b3`:cHǷ ʯnqoz%,W{65L'] ^a!]1L5F骦5_$4n1״f UUkc8D(K!k0)gK^/ d"^1 ۥTC# >{5`0 kJ֭[,Y/*?ޫs& 'Vq̋YwKǟ\Nƅ `16=3!f}f6mڨz۶mANq>|` =~롔g޼yj`q֯S81`8`n7w$?kL]D0E PgP00޾q([jA9/GܦQC/nƳ;<_li{FZ{x+<1a3< }FջNs SAU|]Ne[3/!)(w.׶mw.ppwj'k'.5wDLKxE*&|KAʫ5՚ ^v)t^5ʃTAj)p7׀;ZLPċVW];b8P2j=wZ"GLJ:-c~b^ _y+o{|I^ $*FBu oYbc5Y/xݐ=*cuvU/hC<@GXg,?cm3\ˑ#ɤ^ô{wMplFO@t*{U/Dx#›UU#?LQ7@6D㷹7y?=^#^=AXW @ S\:uD=qc1Ivm=fN\Q\yU9s%Ԯ#b]6g(1lgJrz'c_]1'~k6]1rtܴkݓ}hxA?8Qc-\3F2p`  X(|;Us''hVcw{wEqo;vRO<ԧ^L{rV3OUZ=Ʊwxl.'>ċVeO6W('uEn9NDLxS }RHYAzࣾLpȕy /{tC}acJ=h\COīV j!k^@ױM5drf=]7ӿBEl6tem"L>^Ǽx!{[_U'A114Egl_MYN54Ȓ{Uϡ@=.-sa Q2߷"\<ݐWLpLqMv\zV칪/n.m跰gi>bA3e'ݱ=ذ(3a~ODUes34\]- v{&z,_gC(A>aol^xgFbA f:)u^uVM1=Pnbfz2671Wb/ۼYFe+Q[ֵy;յN("rq끪!x81fS?{dw C]g>Oje*[%&^z<^v 6{XeBeYUgB{%*?sj1Ѷ~<ʧ54C{9XejZ\)F:=cO<)>is<^7R5O&^x!߸/t۫AҘ"5cL2 ֲ6Vb*Nλd/cj8iShW ytֲEDUBUkrqiJk%xdMҰG-S<^rDõo\OEv r\7^*&lu*ƛ8F݀M\:Qׄ A0}>"HxqC<RNc}sL7xd .yMzx0 9 Ciؓpy^I& +,yߠSS9Hf u>zLu_8/8/0ڐ.{X]AtԸœ]]+>l5E8ΞgN`Zk [=yTdz=^wӮ'L=Æ9ֲBHe۷jwRe Pu%c/%wċiR5Z,L]ur^z)~zTCZőKuq̜Xj:wjz8PDro#|TCL'T⩆0EyM5`:9:*Njtnc }S`xqTC 7 yL O]78Mc5:4];{1dVK_!Fd~ {@~=Z.o1kuj{PZձ +OYEʱl o{s;9M,DZA[vS|[0Xrt?w6xċqj7 cƀjԆZs?tRx~@I HHUVՃ685<15㞱R AYۖqI530.T(0V x 3Ljy1F3`jױ ^vO?WċIz"T눵PrG9-i?!xh }$Ë$Y_$);XWBŅ=E512ZAhv ּTs%fWecϗUcTM[h۶;Y0wxޞKoVIJA%GJ7/NYj~KQ$8vyJʞBŃ1b֠ɗfk8v)GCt댖:M8i zL)Lʶ r"^ =qKXUvS u} f_ s̃׊yZ.3<Ҹx5sykƱxM/<^q:Ʉ4_#o]V=ʷx,*FtP$}wo+5^ F6W.;SךdJ5ɢ9xyG#]͌3%do/Y^ˋ@/w  ӷon//<;$TÙ3ghMq8NNx>/~؅w"Ox!u(Hw׻\,^upvOޖ/>\Q/9?}њ'Evڵ'ǫjK/ðk׮9=^_9^Yw`y\qѽw5P|r跼L.e \@qHB4հ ȟ=G;Igm3M iu3H{S.Ň-aqUk7EgoYe{b!>()`z@yh NjW:_HOl\_[=eZ!jmZ6Ƒ .>uPp\d%TCʘjk h~YD5?ADXy^(m{9ML^&zk {"^7k*ͦ[ak fo5]C}{;U!_mWZIIJ/ѪU^DuY|=cZe 2S k_ֲ>[`{ͳ)OZjZ>P l:ul>31sl|‘xqz-%8؄l)1nNn\ffi#:wuiOv|vZ$v׻#B<랎Ȳݞ[lŷfڵ%aw`ǧO7xqYz$ry}re2y>n7 9 }IjBEaQ2ED!ݿ k 'o]U|1&:|P%?~Ce+;]8V.CxQ%"86"RT}Oɔ,aljԠ~w"/}B? i ^oFgVqC5x&*ҍg/zmbuSiT^=hK匎OɒEݏs>:oW?gMU3jH>`4HeM\޳;v%(Ӡty-ӏ-E-ORlD_a%"u*U_}MKvO^MU@jHŁTYvQE RMд߇Oݠ]g(j/J)3#tze*Opdk̃E~-ZХKo֩i)&FCQiHѢA}ᇌZ4i$'*7D5kpooh_8iHK,k&UVZln->b#bbA*Wƒ4ʗ7/EiȐ)cL(:|zУGOܹݾs.; 02.XXՀK .}Ҟ{i \{} jْޠaCߣV-[;\7nޤB gƤ??׮ Y̙2ŋ |TtiPxN>%B|VM.|9V@P^}Ypᅣt )UPZҎx!l"y52f 5?[`` !ؿ?΀:-0`iY'2|p( QI'M˗>"@p\L5*Q a~Nī{,ڧGV㫦?L=VTz؉k/<,xw]vv215t !b^ u1xqrmj܈y~oDToбcǬY9lpmo裓m(2"Ľɓڷ 'hQGKSilP+CwbpG0%R_`$׮]ڛ3Q.?s<R}9K^g+/`In^vPeq)NupW 5ճٚz "5a3꟟\3׮!x:Iձ߫UP)ig(LyΜގ7?zeJ. f_J8(CM]=^'@1ҥOxybO`"E_8 #+T:jXMrag.y]y6NE A=?wEaBϪ/(w"tЗ-{6</ӑ2Vbs.#3ovʔ~q G3=IRF&DWJ)35xξ&OI}Ѣ*?>MBS69(W( rz|X1ϋ/I~ʝꐃC6[ƙf킦8 {`5FU-9|YDmJp/iLڶ>GdVb؋p=DϿ\an]e1ce¦ sȧ nJ{@5=^ܖÍIYRsMjϿx&x1 Z@ @KTp"8F$:y Ek_1hψ֩*GuG2JH堮SO%=uœxaR#hBb'NYtn^>+ Qb14Yix &+T%}Dje۳OϿϠf,. \ܕ˂ <tKmVrרC>cPo _/ m{&bY"r+|1Q?թ.#gvk7`hs%_KKyOf𔷿/U[0wЦI \- {'W6A pk_FWR    x/`D   $!^ N$A@A@A@ !^^$A@A@A@H8BH    BI    px%;A@A@A+xy$A@A@A J8v" #>}zP/_2f@;:t= "EA@A@p/WuAŊO?ҧK r͇z}.$  g BD{@IDAT&m 5kٳgN:Խ[70 Yf%(A@A@x:t֫!!/TRЛoHSS魷^{Mȁ  /i.\-R۽*8qNK/ѠpMo&Olw_NA@A@]x=u/%'^~;Aۿj'-[6Zt)J4u5k֜߿oNNA@A@Mx=ė:{TZ5(**ꑔg]ٳtN8jծEvZmtڅ/tr"  "ⱡ 6jVZh=t7nؠj͛7U~-4,L[xϗ 8nإ>&i H6 m@/ۓ@Th /o2:{}W;mݶR$>ył  DT2|ʝ:wDM_hJ@Ο;K : X9hժh=wV~!5o\&kK,43gΣCt   $Q<L2QE}"%dn`-JHa\yΕc7oތF> |+}r?q|qV>s*꽝V)苏7 waM%2.'}.=YfYD39#enN `7mJ~~9x$,k֬nz|+Yn ^A*UD3g̢W.ҫ۷o+sʝ'͛%}9oaA@A@a֠A=zt}N?wޱipˍ1hJև8@83i k#β!ƌCJGmof퇝},v=FpY>N bNOC&e&l] } bNsirߥ,˪_ջz= csg̤&`;w?OɘM)k.GСCCy%zmBcۼy3Ftqڵk˲A@A@xH"ԤII3gΘӰۆq|Gc\!Z"V2k;; lNɝEV̌[iHQO)tlbEg9ߝdQ )((bb0V6A@A@g!^|x`հaCL@tLi:vjG#rܹssA@A@g!^h?fӨQ#ʟ?XS a3yd_͚t ٳi4hi݆zuk:oh  gޟR*ضmklٲ̙3)vǚu֫^uեU!D#M~FA@A@@׳QO]) (@ݻwA3 ND4>}:*X帒4맟~4ˁ   v $U_|9ʘ1ۯRddds1A@A@!^OsJA@A@A I +IT!  O3Bڕ   @@@W1BA@A@fx=͵+eA@A@$$Q b   <zkW&  I!^IA@A@A@x4׮MA@A@BD5   4# i])   $ x%j#A@A@AiF@\R6A@A@A@HJtC ǏSttYH)   WB8-bE܌RRfNNSh{SId&?`<~]3M4(UՁS,Q2ia]9|.vn*χ}=ET ޽{t% yvRXzVHgR M;8݇hFƷ͠dҥhѢ8 }zf͛mdrA@A@+hsIkQa Ut*]nyh\q#~pv09=J}fLUVyZx̶+wuۿZ/}ʒ#ҟKO .Lm>\.lݝ;whӦhٲtIԩSztYjݺ^" P |}&JfJŊE9})oڼDח/_>jٲm۶>? C[cƌM 7Мso<3f կ_r  #x9jh]p?J'7^rKu}^kvix՚VHxejCP Rmb"B3ϺKQii;Q}A erAl46-TD^ʕƍKE[7oR6O5:ՐxUTC^K/cL[Z7Χnʼ͢6C/n϶ UVI 䒥Kh/_~4vXzNE sԺQFשFȑ#US飏PU~嫗i+a„+DEB\ڼu ?2+\8H!`4b]62Lm/y|Lv# &S`,װFA'd3Okj5ljDd}3E L5kkkCGŊh1 4yZ~+   .m A?[ic^[b}-tؼKWӧO+b$+r%$x!|}ԥK7:ɝΜ@˗-õ9s HIǍk(5jH:z8} 4!3{vq*U)^=z嗔lL֬Y!CWLT,]4=#m=Uo.:+ί_~ްGX&xQI gbg͚m'gM?V.DYƌN߼/ÎD uUc'gM׀=tlW צuնm4tjUg!/i H6ԵH'^=x@@ڈ7M(R%ՀM/1L婛I㈇U*}PH÷=;Д ȒBA֗N\5U]{\&u5p `2xmټ0iӦ&Nj aݘ>j(a/ 9#oӦ Ӷf\sܳ{Eӎ7Nyر}֞)ٛE2ҴmVYcd1;N2ed󞿿I)UyݐV=aam'Q h=p^.Οɕqq^Xл"g2rg%xI6 m@ڀ į`LxM5Ɠhj5͏eWˠī8x!7vKkU~.#ͲM;-Rӹ LS W!;Gsdx~+h iaZ >ȋ|@qJGx/v!S}:tP~#&5㞱7=^4Րmtggy6}iMN"ޢENS:֗QTCsec)(;YӴ1/iʕsQN/i H6T rG;^\lٲ[xxm`zV'^< g=FTCUN5ć~Omj-Z^1vŞ Lg=c/Kڀi \6_Y%;牳^Ө(>$o<^,SʓUO~Aȡ3/ܫI0huE^ j׬]kNjͷxiby@z/g/Hb7wQ6-X?^tzIhO%4C S6|.mPkÐDj$.e5MhI& ]vkKY#W{5^l5iڔbbbC(u4F飏>5QPִjJ8ixђ%Kh}V߾f^xQumѼ%]tI]oԸ11-[JӧOGȑ#I3gNTdIJ*@a9~ZfL#|n߹Cϝ3 0u\Q1OyzSԷ_?ڳg ߼Q˖AC/^7oR&N$r޽{33ǁ3\j󭾴痽vv#xП|si/݀ eΜ@>iNZ-jР>]z\jK֭hNek,wl[m#DQ6)S&x" kׯSҥBJ_.ܹsDA@A=ƨe-Ո|1V1#|H;RKvC;[/*rV6ԯ:j DyG8x֤┶!M1ctz3n6A#PMx64EݻGAΝ?G?6mDqFv5hV*b$;4aD )((VZI&ٻ (., ;/HS<(ECqwwlvw\oۙ7fo"8vtE+O%V¨0k(%:516m%bC?ΝV xQS:w5q@UR A$^{В@o þ]rUCz=LwFP\ ۭTɐzB=-Ν:C4e9a3زy pEC5q#dPLw1[=^PXXf5Y8p (^I"5zQG+?#0#0#@)ۙFkNN"oB,<:}3$a6E `=5vD5_y$Qo/ov `=H(1Ċ ݻ>|"uFPLp$ΝdTi=X#I_DyV( {FϢ᎔ Yng$N  R`F`F |H>(XJE@jxhִz;lj.Y i]Ӏ5_ 0#0#0@@WhHB/o/9eѶ];x֬]kSvu9Nio߾WSX/F`F`1r% WeīPFMxN:k.'-\+.}:#x((X`F`FxE 1e!4F?qhkB?_Y-0#0#|m0Z, Q2t=gt 5P@ q5ugF`F` x}V2#0#0@4"+F`F`F@׷l%#0#0#D#L|`F`F` x}V2#0#0@4"+F`F`F@׷l%#0#0#D#L|`F`F` xa;'N$Hw޵ClWAnl|U_8J nݺ%7q#0#0թS'Y&lPGiŋ "uԁ:}PY! C˖-@:u*X7~|H"vֽ{whܸ11cF믿wpXz5lڴIݴ+0#0@C PZ\\@qa90cZƍ{w8vt9Be @޽r*>/\ [l1 ="vw\~=\eUhܸqPD {.ԫ[/_,dɠ~7o~H(!׮]ѣGÑ#GX\0#0#0baÇvGخƍŔɓ#\98P̝;GwWtpvvz޾}gϞ_|s賛ҵȓ'Oi{8R.]Z,]/v%/]j]ܞܞp>} FgܰD۷ Hò&OE! ;vYf~DBe~'3fl={RJ}ѶWX.66>}vJH'vEFIOQd{t:H=Ta=C{Ex='JH\R䋷=Ϻļ ))p>`uW„ =K#r?n=8@={f M4#{H$_/_6)S-[=:eNpPĿ;w+WyذPbEkfmJ.*H/_Zn G5^ٲe9s@X`U0rHC}FL'M?o}v2/o~PN-Xt;<O>![$I,zz#@JuP]9sfȝ;7C8v(P9 \uS†3#gΜ/.]Mp#0#0@d `x *T!͛d]  1t߿,Xrem>}z6l9)@޽18Gς&^۲y3ԬVZk OTZO2ǎfht > *Tsfφge={vб#*Y5D=tu=n>%t9h׶]$T_jq TѳGpè|mH@ 8ydvݷ__]6,[fvؼefzTR`=tV!C dRU, ܶu 8PYB>I}PC2eh{8\\~'K.eQ?5呣Ǡ/~лW/_ #0#0#QhL2Jj׮zzZ!y ӃF4@^0ó|0=:uuEUCTâ=k=z^PRE3bM6oooMs6-*R Z'B8*ѫd @Y޾{ gΜ7n(9r)RQ$z M1@bÆ/^| l5jr]FitnݾK/+bN#[Νɭ\ ҦM#?Hm)-Ngؖ6֬Y* I4vvȚ% ܺy 8 c;pʔ)a,mB'^ .D(=is58u4^r7oUVm4EBY#A~YL&5zZ;v8_b}ۀ?.C쿣`Ŋ&#0#0#VxYT\DM%l*pWLL8A~KKqj~Aٳ"E_ӧ_~E =jx/]X28|p kpjXd FA'&M$&MjOƍeĻS\zuۿ :"$Ru.Q]C L>U"`_gF</Н0 na;P`piQ%iz=MHXǍ;N]rdʕ 2)%0m$ ZV1vhRy>l`Lp>}>` 8^p6=fXr'ɁiBd1 O?A;?d @ q Mif& 9`ppУ!u]h6x\[nfإ躧$D) QTCY5vPPhfOLb%Cޛ+;y<{>bُDMC#]Hzuu$TE4O ƅq>}3~/& hɋSƄر$Zp&@rp[^=D5 ləFp<C|ӞP-[HS}P=z{Fg ӂ/a3*=$r<i}S"/ X>^G5v(D~ǎklA--TEzw/G秥C)hcXڏTOUl`\p>}M}z&,>]ӹ@?`?~ uԑUrڶm[)+{$TX֛)S&n˙<^4hm䙓VQɣg'M_A}>[F"k$y-^$7onի/G 8о}6M5 "¸Ǎjqn?a_$,CL5&%ʅcI^T6FV c}p07`Ügɒ%EVASN4"Dh=O1Xv_ML/?}u&V u =$cC GWHpњg)~׌"2 o;(k kњhEW=syjHv8wv{c c}p7Al_;4@4nDL2E[NKi40mذ6b ʴPSJ%`zOKx/z!%(QݺK=òFȨ^=䠞s&‹ Hş) ˕(',ċxہڟЈ>_O<=Bl?uM]XWժU%X06 p> '  o.ziQRSX\L4+$NѢEDTU ;}dM5A9տx"~U:+VL{T)ymU$3۷ŋp1A{*ko`X&.”-zv6/HC#^D?^_ڴDq @;}oXo9s4@_^," *@.. eH +a<5M!l8ΓgK?S4:"M0<:ͯiٷ{n r6mZi#MT>Llݼg ְk.'קEz=٦M{d׭M +L䱞PE$Ю{ ۏN6tII&6aeNalp>}ORJtdC %$QTs,{+^Kjԩ+#@;W\&ieKNj xe̘=E{% 3\+'jưxL]SN1 n.C&VXQQڅ#P?`˗/ D6rmrO%^ o;xkz5^M<^j-RW.gl{#)k/"Rߠ=tjU@ #K"PT)B%4!PJy?"իב+S,]Xȏ"Mѻ f͂ g&y#0#0#^B%^-X/GڬY@ĉŋ}]\f#G$zܹ%#b3|H?{.=Uw{MrLzȋEA"[a! D#:U*xgOUUW/;~##O5kCBtB*U%:6olo2#0#0A JWxd ٛ8i2O3g˿zd/mG74HT|}%2 ׶GOȝ''~1֮]k?3#0#0BWp)s0`sy\0D|+%`x$J>|0C)zSԭo߾y畦ϗ2d̄|C~?7ϟ$IH t5Ν;9};wnH< y&ܼunߺ-uQzϞ=7oȤD KBx-<}(;čRHp=<ތ;6J$ǏS݇ABF`F`#xb=_?_ ^ OHnݻ%5l)T ]:]>}HB /'N 5Y%>}&H@,TyK,#GHjժA D#$ Ȓ% tl^}C 1Og"=P&~EKG ?~cAH *0z(:I:7Qϡ?~h֬pC9٣Ktk(89ŗ? N<6ldPf?#0#0"0""crqI2Xbûoap54h؇@IDATEW/_ŋA)r_K,yڶmI'7^ö۰2y `֬YZ6XA  ~:šW%HBSmٺ6o,逄5Hh"r(ī'@8w|$,zB9ĖyseMF`F`oh3>"K⅄`pa(ã'$©zO-!E $4{@$ŋ0idG8qLS-]ϧ/"<߿Cx> dyDP,bܸq&dɒA-B>:H+ zGY!^D۷C*īyEGUBJxe5tP ֬Y {bŊ zuB,lcG_#0#0p?x)I)?#HV^Iy2BJ. Gt\M:UNn?I`R$!/GϞ=aڵ&˩zkچ"Aī'N5 5sǚ/*O=t:Ɖ 7nm۶S ""kJzJ\0NE)؆K2N?v; i Dک2Ъ 1]2[ *$7{7|x:>װՐ9I6}AdhKZK7pJ۷mKOR,[,ԬYSN1j~5ό#0#0&LQ\*\0wȂ;w(A**ZbFJ'O`PlJJ*2P gNޛޡ/x8~-^ ;w3™g`.FCNpo")|O HB{bYjHu>(xԠ5n+ppH݃'cAɒ%oΤ* [lЮ];4_Z ѽyj`!믿ktys2#0#0#+}@]W\d8EK%%/EPxx)GuTRf%xPzTC"^dK̙cǎ6)^P)5LEr=Kek.m|+ZAV.TP ' q=Vh ,8|hS GEfqu Q  ~֊,P !̓$N,+#itjH8ҭ+" =r\v #-F5ƋtPu&LD!3zTkԍ͉WժUJp 95-`@XR[hCAP5^jiq3gL%;?z4`F`uhՂqm%s҉>O<b*$fdzJº\AL,At@' lMd(Z2Wxϝ;7<0|pxVl\5T9j^/'"7_s.[Nx͆("¨h59a{"z>z`tmڶׯ̉WT޽\o[769 (]4zkapgy(ȹs%z`sDp!Y~" 3#0#`O(}uh;3~XBՐ@ aU~(Ƌ6P D+ &At6;vZlР>n\L>oݫ'=^hZ$EHJ^W[l}*iLḰeaeV8) +qZsUW'OE x1$j)OaɣGd<^x]pӃ0 ),0#0#002Ŗ[<6F#s2P/׵KWW֒>^xZ)uo(-1 %<~^$1g<^T.v! N7w?EeS%[W5`!Y8C4{li^᧟~o%J@z4yBҴ@:]5j+￰~zy_:Hp5xw/>2j(#0#0#`/Pl%7PKXE~ čéS"6hkx6Cț7/4oi/[4ɭ/"ѩAk0ƢjeO+v,94WOkJ, uEsBuN#nC| =^-E0]S  @Μ9ep=:5 N5YzSukee`F`FAW8<^eVt~޽{ >x~BEW i*g~P0z^`gXh3#0#؄/`DkpՋgϐ|)IDav3ḡwn+"xǰɐB/&1~ܾm*KQ[jn3@g!+RD۷@e!{,2(~WFT&*zatیϜ9#?0x;+xL?°޽y UDZzrRW{ݹu'OObNڴ,ŵkW4nL+www(p0eM86z._ '&@z-{F`F`,!2޷H!!1f<~Fo0W>^>(TC] &Td X/*<իWbH#S\~ Oׯ S&?JN,[GXN,Fp AJ^Sk\'Gx⹜>x}{jب!ĎGD?V c-xv{q<^exժe\C0@ F`F`P`*DEs5AW $pYpI ًwý 'C22G$b!W*=KaT*"?a<^<d/ ‹o΅p#82Qjr.V;* R'W#ڳ|A/ژ :}'s͞ŏ@o;sw+Ⱦӡ}G؆MZ헹+|O/em+N,]6N Nk@r!gٌ8UB8} zErX1o" ˡ^E-'W=Wy6\ TY(Ďlz1*kkX(_ nݳ|re ;qԥ|q}{6?C~aߘs`̙V ԯɑ3w6a]j14ᯱcBdk>tuU' .1%cpt(zfH8r^A_;CE/3KNt/zh]Zِl_3$Nyvޅ'el aLS?5& Zzfgx?fԃm $4)ݒg9:gkh\;c)+ DVeD&?$٢K*] ,a~Mj俐0=8аwC14QdhzP]1쟟{Ʃ{;r`iG -[=.$6M/G8zLs̕<9? }2Q„7.xO 'NZ_Cb䦢rV c{l9w`RzgQG<ͬ?Qb88t:% \.dnh_\ذ ߅/ET -Ь IEa\i?N`pj* nOa{2s?_|xI+ʮ3D+Apdy< <@8y{U#HFe:EVE,8L 7!3M ($|}Y޾fٽMc4OECa7*虳PLF$^kxcFY轥AjՐxMA5準Va6 !ɛ8s1ԑӧv̥7ӒXqPuoi?M&i[;c,X#Xc{TJ.X[j̏6㚥P:I$4C" [W$Jl}PG7ao.('**WrWOmٵb_ _̴/<ϟqǻ]sIknt7~uzwXfƼiL}q3nd.j$7:,XE?KDKdW"~!Y` 1od2@u SKǍU.}mCk̮otɑO#V2xHJ6,I9!j r~Re14"fzY!^QCLr3Va´4ylo!kҷ^rh}gjߨCމWpm:R6ϟ#?DQO3U>kNY̊nDWcvkٕ =Է| 6LՓr>+r\>ϟ?՟:;8{Nko=#?9>Gt/(H]sgCl[['^.S a`ۊOs1H$EJEڈt /y%#GG!_Qj}Q PJuE+D#Y[4I#u-L&rB{y_$SV^(rHUʧ~4xN"P"^$hN5B"]^ /GLI!ھ|6Kmam? ,+u(& o/S>x|B}2Qu&2E^ {X/ȶ§oQig'_-1.)4P3KӈNaz0ʒ迶xЋ~U[sYءUbA_8mȵ&S/? yg6It$Nbڕd-FTHW*Z3,g5oM Qu.׼k;ۢK%׶/nOtSJQ}\e}zan WQ3eO؇}H}?$z,hmmH.3?珈I<&F}{ȾIvsr-hy0?<|ocg|e-"_Y_e9D0e3OcR?Ӵʑ8?*߳jZHg)w oZd<"nN Ȕ0_l шWNj 1"VM-Oաtg(,Hn^4qSiD/-a0xc+].:A܀\߻Cpg$P7-=6с]p\.O)TTp[f)탣Q\dt ܫDx|fmU?c0pv T~:|/(SvERk:$>fU3[gBl) mr>bl͕84ZJ w.>~1kk_oڷ/9ڗ"Dpd+bo0t3t6_\w[*htXe﬙}R'=ué|JC/(xg:GJ6}> B!ɾh_:hGD}}cK^{/uG%:uSƣ@20ti0=(#NnYA^ la#Rjcjk]ni"p<R;>k%1lu&]͹턚𿮥 f4(r6W}L7Ro#(D4OE[<_>Ȟ1xс+(!@BaPhQ8w*8e:K(ڠp&5j‘#x ֮'t&M)>X l"J 脪y{aCR)_};l嫻Gϸ|A>ۮ^ oDZ͵ {ޟ@Q{c:#[&㙺usW'$=IYKVHݼ~9%Jl7}5ݖ3P=Y9+ϗ(9YP=JzJ4(p,INY-3Vd A0}FLOiW6^DGFizp&8߽⊮QF3 Af_竘E,xs!5_"OR ԋI[rcH GZ%1/ClT}S5_G¿$WH?}J?[SY=%Iu}8})C+7F`,xOu))H- _VNMt%]n_p,>_~-C,XX{cHh8Guzyii^A寧uOˉ}Io:=ge~:Xh럑QTx3ħӧE-#~wuVAtZo…Ż8PobŤSD ߃36{:(v5WMWϱqz. NW2|JW,BWnj_{wxz }4n3G~V x+&Xgo.1" %Q? 0Q/*ЍF-iH{%tR{-pSaNF#Hmq=9geP/$nA+P#JTfa3ڞ5z}ZXjD`)r3:֎$ye/[AVTߗ53Ii40}҃\3o_FStC>H8`XFD $Κ]&!GIea-OTZ\zif|MAӃhN8/x2Xd)lY+w) iDyܷ41[mV  Ԩ'}A5׭juN}1hC+1hWAENDGd޼ΨwL )C ϝ4gǐ .?4bHu"U{$ xDx{7G/k4jvO/Cי vN5\$+$z&rc^FD>'JN-a?HVo h|IޯSyHo KBrxP\Dt Ƌ8ppO4`\R1S(ō8(ܔDiGkEyԼ_gwG9x_f Nj=^ʀG{0P,子E.'LF~r,E0H/GLT}vu7ȶx'ג:g+ ObBL|Qgw9`ޯ!\SHsUzZl?|Τ}3S4@W S%5[fk_:_D?O#^D(x _\""Df5Dwz1ȳ{w)K[aGͧ?)T=/0+B? Zk' \HeFFZD?~*?ST` +'N2AZs`;)Nmm S^s֨2BxM릿.[CHj>5 #!y^>,B*[HdQYIOL_8*$=DmI JB7E"f̗lr`2gͽLF =[fާXk?~4GӃ} C=[ߞnT篜 #ƖmHmR^gf3_[#ժge_ċ}Ӯn15\g}FڧO?5%BE5tպo9SKu`Ad=<9ZF9:-,]ə/y\Uh dH.VuʞggD$D:eYqzeɂ5:~,}Æ2M{~>XQ=b xaTCjTC#eKMCϴ%BaF2tց)|[F#X˩JDA9y+Z2 a\k붬T󮢪pI(>/W2"9eB݌TT9}Qqz 7/?Okt/_f4Eg7] 5&T6p] tT[riD$͚=3YủX?NyzY"_a/#2 w >RCRK[u(kLdSO*.kقS.WG  t`UZyQu6LV >MʔDu٧?/ϟr`p;^ZR4*qg" Bt*cY?ܨvi]#=5hs):lRHis;sx?vz6*sHNN[Z@Oh!ׇSVcV "]t7LnlE+dϕy)WrBoѩi:gHZe^WX>`ű}TɕC'z&;ki C{ܯJWsI<:/?5 ӎm^vOͯi,Ih0:OMӟeȂa}ŀ5sE0ղ!]w^vHN!h~zHtj]֒B"i+ BrH:.- Ae({_*M@yeP8yz4xa4CEgwpB 07Ͻ D4 5 z!H+d,)\>-*) >Ե ۽#Fʞqo4ۧ Co2GkEShMR~6طk tQ={/ϟCe`kK P#(rEvw/(|#+/ '^j֔DS6elN^Nf~#YumXhaFǻ{wOFjX<Ɵ={ʲZۘ@ċ^*uvh.g"yZU7UM^ V5IGr<qSa oZBQ{Q}N3ԓlY g-> iH|G~J;f_p#(*uK]~ FӸɪkJ覰`::8kr}?ϟ^o9//2xUzKY3"ݩ թʔn_1G<>_zl?NK "~D?Uj[Tu3/"U()=^+o( Pel&ʼԶ.=6yQxtK@9@w?MFo|DRJj8u/n&Qgsm1BHLUm5:E QY_^x?U~V"BaMWe)4KH!W7Pm_=CDx?A-U^^5i̳wjP=]S$O,뜸g$mL(xxwA8gx"$='b;VZo ?B</"({h"sQ7X yf!,֨$;Hx2SA}"o8`7d܅7 ]o) z9ci<Тjӟ-eʡ}2>AbSP䐰7S^/kIO {n ^Ey)M/CrpdQQ>hiMUUED\EM_;GRTT7- zVթi}.߲\NգD{L:!o7A(b2=4<]b1qZ^t˷DyI}d"G M /k"a1jS"j cPNj?@. d^v}4#Q-}e)X֫| >z }SLaHyC{_X?=T. ڣGJRMz =5+hTC\3ٹIVK/CՐB񢼇^s ҡQ#+f%:#A>\Lh}v_X"{lKBF^-J8|s=S5Ht6G' jM,;?q_NN!wPʬ.wY~Hބ{7'Ѧc7l"ޡFmu{ RxSW/Elx/#n䙲 'ЊT%E&U\[˘ \$Y(6HNn\T~@ #n^7V?êx8" :$Oњ9怿½+OĖ[Ȑ3d+pd8 ؘL&N9O L]Ju 9 ~u"M@EwY}V/4.TjS W-Wj($?H!]x"~}9_#`#KȘ?-Z &/VŢW+ hfpr;ڗG/mc襳ƟG8:><{ Kw'W9p(I_ib nѾ~&i T*_+w!E$myC{*+lm SM ndQM2韤hxhߺR6$컊}x0Ch߅G=(KдlTy´˽4v Ӂ˃6[rq>lu޿%w`Ǽ&j{mm ]w.>?i?,DpMSblT}6F[mRvG)gMGR<?+g$EA 3a]H`j۶&yj/8ub[y>x9̜)`BM8eT<Š{g4^x/Iu[&gkr呝Wʭ@fMΤ/kz kט//3Eep0¯̘mJ rr`p])t" 9֌ s{oiz&ض4~YXH?* *?"Ot}da6EI:ZB?O}d}9w]_W3KLH`C>vr@C|pf5l hp{xADt`ih~M+9I)I({ ͱ`zjh9SuYuٖ%XIl_a>ο?vgċ0rZN}>,n9t({^Ǿ y9 >YM pcBrIGW_xr׶PXzm Krwl< (0Bըċ~x+w$ {#&4<=y _V/5Q~KBOOT.hDUK@Gɓ__Z6B!ݗ9K~ߍ7X=4Y:r\9z;T$ H@gs| 3Qڇ^LiQϧ} r̈ޟ ܾ}> }8mIz)#?5$wMûej=rNS턚}ооi_?վ6[ ȍUD. Quq˕ \z,d9"*ƅۄN+H2!z5\u =`7g}Sf$I8[I+(3eWB 3"ڣlKzxG5&z`F`F`F3!5FL , 0#0#0#"2©8x`ut}#B2bY5K#Fر2)(ڴQ-J$R{{sy?_|w9w=#0#0@H"C C1԰BŒ#0#0#0*5glV6 p1᫧Q#0#0#0@2JF{\`F`F`F@6t[F`F`F`R6RQn`F`F`oF`F`FHmJmD=F`F`F`t`KeF`F`F `+F`F`FA /@`F`F`F FcF`F`F@6ɑ+7<کydl @bu*;_6aC} V@B&*SLP|A( {%Qw/)f l}GkYv݅EW"u6(UAl ;Ňpe~.#l~5QwpB+\"XXe{+2#!nh\ HrM5 zc/f/_3,In^WWO!C _եCgSNLBmU9lhOAIe|mFp(Sr`kxD_9V,glg2q^wys䗲E21 ^;l@*w4N=vڙ/O$$܅erRtRuLpm*յ~)FDW;2gL|T#|,=7j5зw#yV<x/ZKQ'Pc u0pRzUK] tIMSo䦓)֮|5|k;B/WXMLI^: {/%7ȋ'~87m5;upXԵV=S8V@,^xSvIEꇓ δstg#UwlAoz»W3~*GC&ovѦ|կ̟^=>m^\V iUlY֣|<=^b[bL֧)ˈ}g*;UU^vn/ӳФ2:[{*KZGl~gNk=3*-6MX3֟]qgLse_Ӈzann+Wm/Bpo >g~9*]QeXg@zNAo&ʯ[AV: L3 3od~ѪM5ԆW`*]3J?<_L_0_l/zw!1yOt#Rړ!)Ϲa1|V~bɡ^?INgƀ*k{߮ Dc)BLH/=\gx~q)s!P,ja bcë'#QL%Pߏ'CgJs.ۢ`W dϕF{a8[eDL|uYXa{wK.J N\C |i9 È D?{ ~_;-P0l9x|€zcN>xf&VxQ"$&< IBYŠcn |ߒuuY 0޿F","9}x8.N슀ܗErs!S&3QyD^OzRFx~H)Rg_}Z'9--ءno<g#Q:o,gq[I=c.gavuиgڲ4 &9.0#HHP?֢ Ǐ!6V5p(TjWr儫nCŋF ,(԰ 5\kPF.kkprp+KKxth&CR;~ C;o{<>.9Uz|** .CK ϣ?nbտ0F"%Đ'0$OUV9O?RT`N\u^݇ Kt {/ZN7mgd-Fpw&ZF$Zn 9d1.>Kʓn+j9.'kGO2"ᵷծQZ -V?$1[EC-гI^YdX~Ho\+ce=gWѦoF)39yf13xl%I!:J}g NRֻU6|qz6})chz4Y _9t$H%7=%$Fc6vzLDM>.g_ƛ1c`2ddѢ~涱(,Ѯ`AAޤ3fa2=6sv`*KIxU)[V z8a^)^,:?+Hp~=pB^ٲU4Rҿ@ݪ&$k!Ó_e`onrK7Teȝ0@#kPQ!S,8nD^tIt&:~ Kj1%8T=^f5$7\6K9 ]`A_!DZzS%լT Ju_v-5F-X1v51jw>ZTi|*uٷ3vBuzYu$dP!R|S^zJR+^莰ɥGu ٪eGNa^ĀWn(z+1YYEo'Glmf0vqİ?Xo3-y5p_QE)d|Dy~\Y2oFԗi+LV]<*"Ʒ>Nխu;VyTQy)魯IJ{t @t_ 1hy97'A©4eIJE>gLRq׫R rj~4FV ԓ;P??-ݽ$>@6x c@zZHb€" gԥU+z3@/\5^9Eݲ+Vyj;gJ|c(q%%  @i iΫI)Р!X\gm{`m& GY͵4˥:mJEmv@J4$ 7'O~-@z|QʝPC?9$&z]+oQzҩ \rs!2aFt'HBF?2]Q׈ӭܓWNr·FS _,Hk!#EV^ !g ik&:We޺ӄc`)HG㋰!~&j(^M۸~İڡv/6zx1Vc@^Ha{7OXW4(0> qAu|E8NSnH@lWB h \e`H# ~^.2@ȸR52*c7"VsX'z,](m#%6T[=maa፽\˅|9xZNx&b걧R!?.I.mzț[{4&wu#J>jP_Vݤm)r1Aj{5NԩoujJُL^RG20yTx!|\|zm?|,)AA.C$}l j))FD4DZ; KAO7ZIqurN[ewD/fx H1ZZgEkt#j<vDj0!vXi4X&>|IN+G/?4iԈ(߼468SB y_ 6T4Q"pVO=XP2`G_k{nb7Cu|3<Ԑ'aJK8㌮(R(7cfMg3n,'En7,ks+<3L?:<^Ҙ2FAVm*"/d;#\GteLw֮L~ i 5LOFr< /D+:O<2N|tV"OU(V&H7PC5cǀƀfaտSIB c10}w&j/O2ڏ4e9\C㭿jFg߲F7NGrd:X6.Ba𢽫) Fԃ2ԮiD4jٍj֡0G ?d}tbmz]7 EUX WzP #-3a$WݥƚZ0;XoJ˩nR}1V /"LQ Q4PQmY%7pNĉ-&e>cr'%=(n?# t붅TR|W z1m/-g{᳧0Jc},}(|̌>_?\'f^zۗ- @#cMUxyyg 7N706<R6r>^q7UDqvM1H =e#ϸN"8gpBd䁚x侰)ɬj۞0>ARn_1ضRNgLƉN7Plau6&;cl\2+\LK`אݥQ9=xx_lx}) !=^Et=S9VfZ|S!4-S+T]f}Y̳hUh'^<NllF~z)-hlop#TH>a螺C3%q' %g"נI2Ѹ<64ќAAep=guWzqdQߵ+_H IԼbN]Bv8M_Ly\׷f̈́%f47瓮LdG^ʧ1Z׭onUղz|rg ΏWFDhH:Rءn]*G 7ՙ^=SW3&Y Î3<b |5 ebFk!&6n^G/}ZV6?_Wٶlx7X+ބ N5yf173Rex$ r.`mTؔ oV‰]JS:6BC@5rh›AC8x=t9޿5Kn/ c7O5~Xh+!c=p&5Y S/_sA@Zu(\*ܿ-%hY-ܠW%O=+)9؀ce[CpԨeK4naQ P_(@uA$7|M>^PUik/l?`y.0#Hr`9ph2rAG(0Hlܷ|.AӦkgY{S(_$r2,pn;k<h93z ^thhA~Vc*oAJop܆@ZuJ eJp3*+b / n]ժb6t/,̍hp ';k=rw5v>q!6m ܿg;jB^fwz/PjX'n3Z@~dSgK{cqv..sr(u gwk6ȵxo+mR) WK ; =x&5N_CDK9WhR3vu'dxAd1t{# ^/ j'2TI[`AMFlB/4/gaXUmy!;0-iдOu&xӕ=^ /]h`tA@^GCG} /m`dl//p2(3e /X =Ր /->e>ePJ_wYkSz#Wn O@j+Z)(T"w#[J3n@nb`S2g1o[WeTy-տ<ֹD2G^2#0#0@!(>#0#0#|~kxZbcbӨ֘`F`F`T@ ^+&F`F`F?ٛȴ@IDATNǟ{WE$YhX.d]6eAV_F]ȼ8;{u%9gysˆ! B@!  "ApX! B@! @! B@!p x! B@! /B@! 7 XB@! B@k@! B@!p x! B@! x^2dTR}aotiQiҥKti~Jd(s,tu:z:$Iʚ55\BϞhp/ ;b}ח6]ZJ&-]z?ڔdɓQ̱K2%e̘QEo_fm8uJ={v f1 ._,B@! BV +gATt) g7&d>+=AXjԨAUT(2dOM6u"Gɑ#u҅ o(/˽N![nu;ZE}]v Q勗i'Yt 6̵EqR%q I鯿r͇tsҞi„*]~})ej݋Gm9{vlA~!B@! n:U=}[WؠA}*] ]baܹsm $/_-[A^m۴ ,J™?K"E J.̕^E_)'U`}lKB}®$G}ZX?q u^,,/^H}F܃p_hs]-[6ֵh{a5AS+yJA:4X3eLSEŮ]OS Y! B@!_H۷ Pҥ*&9Yh|8;vVl! B@A@W<)9^Cxa-[(3[oؼy͝;`Ѷհjv#Z(wԱcGCz^ٴjõQr4K6WUkugp bWJ&lw`?ݛ^z*.Yv-XEe*x_Տ[ WC\9sQ.8I7o}H>B@! M# +sT2e0+6^EQlB͛7lƂK7]ڪȇ&X u8#w9M ROϟW:s}dj2c3>?=LWJpÆ TdI/P6PBſ2y Zj%GX}=kcbT=_& z}ˡyD ! B@G@W}!«3jB?` +/hᅨ)si,У>QIZO-[۶~_vW?x\y;ڵk"1;3>eB@! 7x"ūlįR!y<0U R/2u ث-LޭٳgS8t|[q~v+X}PaE /#rjt>NZ `g}*^jJW_|E*W,M)eȐjCxY@E5L,9xϟΞ;G&NL dB@!  ^iҤQQ $K>[CPQ yׅ OK8׬YcNAT9^[< յl_PgK XzW"SuuD5wW <OR$Ss6s.^֏[}bsj +WᨆG5lݺ5,Q ׳Zx1ٓ2;.ZH?"爄/«Vxy׎(B*\R%SI&|iC[?rL! B4Fs)Q5~ NX`0;{>?~yxJ&3!RcҐW^8~=_ũqDC>O^~#>KJFf}6Uy#{UNOҐ?հ4kos-[i[Ν#uf3yTt5O^s_Q Q֐!q8=:`%8"*t _̿Rk xūQF*"!WOSէVҔ~'rTbUy?/JWx=::`E\fGPL8lu>K1׿2z.OuqwsIB@! a?Z0Njh\0Oމ6FnT3|<6'yrA/EFF,3G-?ݩS:}xG:Wk3&KӵȧB@! @;Lܢ w|!@Q g2-^Fn@|詧:+*Z(5{9{^| Jxuk#{0U_3Ll}O<=s}Ά+V6}N-f:!xaa rY )USVWCp*D! B@xSx!|;xq-DQ 3S8]9P$ZB{C'w줩SZyݬ] yqG5,Ca:  X7j(sTp O8yGVȀU92~0[]t11t9:e/HVNOE:\\>˦M,}ׇ/5l@qV^eXVZmVh«"(9^xXr~_zʊB@! -H@-xR&EDDo#_xkIB@! @HDxK ! B@! B' +tfC! B@!^!B@! B@ B@! B@D@WH$B@! B t"Bg&9B@! !.I,B@! ЙI! B@! @HDxK ! B@! B' +tfC! B@!^!B@! B@ B@! B@D@WH$B@! B t"Bg&9B@! !.I,Lӧ~J,I3fm۶֭[iϞ=H! B@! +!(\03ҥMkKߦiӦӄ leC! B@k@@H"4fhJ& mذ-\DJV[RFƍBT! BN' N?ҿD%0ydzvmѕ+W7nܘ:taԤIcq;! B@^r  w}4s U冴~[z/Hݻu͛?n;.B@! w/^w﹗H[h}rFYf?RJEAt!=mK'B@! w'^wy{-[6*W}t՛ҟ3giIΡCRŊiݺuYAYŚhN-l! B@ܽ!nDq} o݌^MkTQ[] ¸\1h<Y߬m瀁9}87ɹ-oټg;Nr*_o>?zyww7`lɓy^[6u4x-ZHO! B@@aAOWaVjTLAt!Zݰ, U|Xl9kLǖ3\G9}v4:53gK3f&k>yxlg?OBT9:EOwUClZ>I]/6.:oaW,6}-o޼4|0Fy~3o׮]!l-Y2gL}eɒ^mڔr7|[Jij[na1YB@! { `w/Cʕ+SZP:[WXESN{&ӦN<@n=EDS_cǎy-S 3Z}ڔv{LVB@! ^"sG ?~%|V\(KjA57BEW9ΝS&MTn~Τ! B@DxWm̍V`{UoV\ASLMˉ/mK-cNjk6a2eJ={6ȑ]?i$g-B@!pu۽۰pإϚ2%. >{1ڿ?iӆiQ/ݻwz^k׮Q_cGc]uB@! ; ;mkºz`e͚ƍKyr_e'KLGYJC3&S;m:ʼ%KF8 2are:q]tf4ѩS'jevrS뾑ʕ{1*^8]x~)VZMa]-W˖-A"e  ի)‡~J,I3fm۶֭[iϞ=q&L2Q11t 'Oӧ}/o{Pc.2_s<\ c$I*U/QsK>Dwٳg]|%:2]tAڳ{7ZXYeFiӥC\v>ͳ1#پW>^m۶W|^ϿШQT]ӫ߭8}1 1rjG31cҮ]|]MayAL񯔺)tQnuQ###믿N5kTefqgϦ3g9soEu,\H˖.W==K"v#C$Du??ˠғ[Bk}-tb\k=,*(B77|rf*غOU~Pj8J6'?ZO;gߧwQT1èQ?Ŝx pov_oĈT|9^&-gGQ):[""/˖Q޽n֭y:.]L&NSpa=f4)8}t0a-}bl,[jEY8?OmRv"EhҤO?u͎304vL?eJ>Dӧb?M.c:qbriH-H?@/Wrڵ+ըQC I4B+vaѣGA<=3Q}bj/+q~npO4ӭC2 vHaf*GҥKiG{f}2w\ʛ7kdUҀ׿˨W/ۈ#k(B]}s{N%KBSN$ZCwyzz؁߾}iذaUX! B&;W! (OL7<]8dZP%[<27nK!cZ\וWiIXתOfTu\yA~Dq,8SSaE: ]:C9+eTmAsvLǛf 0s=d R˗.SΝEf/vڴj*ڱcGb˞=;}Oga0p XBYǴԡCR[}74`})R=EW~FZh>|jي)>q,#<|mP@X;Og|J7O>k3AN6-J>l8CѣG).6l|?8k0CE|>|I = hP~LL4l ֡J+jRf^pyv|"WHjbRFOgmPη7 /U},:Tjr}2(zY{;yH&\e|烧lѢEiʔɼF;wQd}F|?k`]x("EPUh"? Aɓ'SQt*T@p9SNv:,kԻy߶F󿜯7m>Oev{#{2 Uvq}wi͚57J)W!FVAªrFS< u9]oK3&-^ܢRc+g OMpmIQծ'5"MUKW X'F:^ƍ [d5cƌFdFceஉT^=>w ~k;7 Ƌ/h~vyQO<7ST)'l!mԨ:acslX-7r/Zlj}矇T>|z;k~?|:׸1qi@8N>%W³Eƺ ̅/^y.+FodSCA4J?[hn_iH#`;__o>{; '|.UVŹ[y4N1r(-[vO>j>ڮ\ou~uGZ)пÇu v)]7]"@h'J䭑Mh,?3m7Ik «Fx\zSqUOTKt7N[ݨm%0 @©]vި+{u۹bﯿ"z饗|&T{{BA$`ԭ[7kشi PfEk|t[Tf.K? ص[|6nP"-e皏yBX_]mU\xޝ9IkrH|vh{ŊJ=ן>!6D:Qk_7P_\sO?t-$u֯_g,{oF_PPr,+@x)QoXnꦱƂ>/^=oy HYJk 4uV `9^*ӟPV?58`  Td&uk*b e=5+s5."fM?~>˲ehe 28~_`>}Lixm7CS[> S]vs2*` ʕ`/#yr;A>|#M li g nejW*ϟ1>#gѴiS[=ۍ[ Z׽͊![u“E Xpmu¢Э,7.nEK"Zi[ΙϭvdWP[}>ц@'_lo By횵7t:toVмr_~ZesZ>YqeXKTq/rƋq6%8/Lή\|אY4xNɌ+ڮIgnkZu@Bۭ<Rx / Vv5aw O~̖&E߶B)+PY˂^U<+<Ͼʖp匌&l#,+^ִb /IO/»OӟH C؇i<3f!J*V=?f(͚=K-{6.gڇ:PK68Є=?iziy-n l}Сeq?8? *d|4c,6Y3gs^?ԏktSOY# Xs_zC`Eȑ#T_^pE Ν@]{YZ5}x繗ׇ 8w6fJxm yi_]p5?WCĮ._/g6׭]?q^r خFWv5u/m*>#{ JE1]XWx- sWfjE1X aU_3s2ӒmQnYoc?vCf횠t@Jxq% pժ?4@x<7- cǪ'kf޼yj0|sʰoyJԠMuU)cPY U?RbHf^F%F_ 1f} | r' *ں~zW^&7:Ju?xH~`IƋKx5z y#RsjOxަ;cMף y娆J=>Yɫ2U8/ݽ.>v6o;1|bI?0kP!0AoV2(Axa9/ .Rߴ[0`Db $0mB\A pTd *||cQ[ [ ?G 3P*mÆ >38qۺ+0en<r!wZ?-H|E|s[ͷjPK2?>|VSu]s5^zOu}}C턫a`\h'Ω>œ<|'ק>:ݺ'ϯ[9qHu-sh;qC$z/ƁS>|pB+4sx? 4v ~Gҥ ~#|u;7Z[׮wL?XѦuuYx泮c\_jԶCZEVö{A6.l2|,#XOľ7-SZyu%h8_ .:S=+bcӸ}"y_9> 팽>7J8Os`ypFiaRD\tOy۩-Nar-:ӷ*z>t8aʶZxY86(>u{<ep{&Ɣ)ftWT)e,q+]|6^Kx]eQ Y! !Ry|9Ōjpx ' ˼{V:投r7O,TGsDXxշr˗X 〧r+ ҢnebA2/!~  6E^.#Wmry _s20`1݆Q0/p ]Vf*7!-"| I5'ūaCqY8?lS(* woyۉ`4otW4&NyGcbU:Gkf1GC\m۶NT.uO1qGjqp!Eggeהۤ=_b\T}\6o͇'n<5\?"WC"2A(?>Ѭ~^yk;8۬㹭ˍΝ;+72ZgB?Ն JP_*:wӪ&X_w;}*wߨj_e_ /^Dnu>p+ۼ;*_ӭZw5:vr r0`9-s<\9|րBtQx /^ntOx)WCN-^8  V>e0ss=;+9^jzXy#lmu)׽vrMme^jyŋxp5ďUxa{T^ ?`ǏUxGnE@9\~Ѓ^r2d&~^Rb)ᮆx5ك"Ӂ?~VfXtO>qhgg3y/cF=ŵb.l66^KS]D5dQ 5'\U* >秲R{Ï >a~DĻڵm5K+ ANg[Yq~.agG&ȃ(qtx녠͚5S̞=oֲ;Ρ/-|py8 5xzw,}(0~[d=sgx /_p|N&^.`AB/ xA[yn0O,PY2"[,vExauͳm;~<^'Dy<:[g$E䃁VvH_<0s,dưaCU!0ƿy-ON9f[Y@[xiWPxqz?= 5]>g=?6sXQ?!j-Bw /u]V֭2WC@K7,f ~{R|- r+O(z}>[Y6_JNA8Xԥ/t<(>XNf![t}\ {˛͹?*8ۄW5x /OUt?8 MV\lպAD_֧kB] ?GxgdEe>G]XM羔)SNk{tbyu4[wd| ִiPC:Ƥs5m'g-Ϲ\&]om66H;aDN ݿ+V}|S%26ퟋ'eD|Cpsۊ _t;'4nȇsv0Dxzmn 鋧ܪsBjq%!۰N"LB &gly'( ]_FvS `1r|Ӯ ^zl\z' h DtMC /%@H@HpxW#ϛ\ !z+kT]RYsD5ͮ T«H q/6(̽?EBis3?/~fR4$Ç{ۈ(h!1k!p?"_Uxqq /:Ѻ@IDATbGob5{uA+Hx:_NyUp)ӓ!B] u/@uG :<O,(.2sO%@$XP2# תP9TrE5T yx"b~xBpmc]Lp^hxc~ݬn /{ޝuA`M /X" /\AWOg/TGx2$xK^~ƹ!iWCsLW$ a.0Jp'|ixy\ =P^,c1k 6eAT̙o_ ݄e0Zr0?:]xp*rmx~ra)=- , :PrkPF9;ovB-^ íOUymʹ>C>+ 1'ʣNWTaת}> -zƏ +!4p|}cOWC_~gp G69괩6꼱Wlm+^_.W"Aק<>s \X֘[Hdd{MEg~8rhz,1MO|+3X% 믷W`0pĠA[/SSDË:M7V 3Ne:W5<źpgkt`ZXr2Lz%Vxa1U^X` ~2Apdp_?ҋٲ6z/C`t}u D? Ǐt|^/y?] .s=[s4_}؁~* uz׽[wwcgrqpό/\G}c1̧۫?M.<>q?k^PYh'Gm תּ{uܰegzqҤI> /cmRO>{Żeڌ&!>L@ YKÝ),x:[GoGM'NPYhQ~hΜ9sut!:t %M}Rύ2x-\[Jmyfu!=lD6o͢ϻߺ‹ؑO.C=z:ukO?Igϟ lC %v+Ν:O [_Cyr禳g*V{%Q ]?LWmvn.Tދ˩w^iE;rK{!}N/_&~jOʖ%>E!;-z{wb=hӴiS)%5kRϞ=mvٲQ 7s>ܦHsaE=qGit? I{-ܿ2eU;@XB\Kr' 7C{3h<5U]G鬯/,4i)Gv߿W[ܻo^b(bK+_NIƟu XkHdǦ[8*ח(Qcڴiev}悼ArE睅]v5]BwqtCbaD~)„3?$2eDǏȟ"hib[ Z|I{9uw"c??(o^SG;1f\b;٥5EG>+{W|<Nj8:.f.NRq]\x39gbpM:D?g&_|Tb9SZq8y;?Epò.pӿŮ| ?:y[R i^:9gnt^/1^E1gYxՎ^NWANN Dׯm^aKUVOwWy{б“m>}Ըfм;q$&g3fuvwr48y9:`axxg6h];cB/ԭKlŠaztq *_v]cƲ@e=g=s,|^F}S3UWbAaktY|nj=f]bЬ6 )u8'1o ‹'?Q쮪 Wb"h+Pۤ&a V.|dѣFSAΧr%g>v_ kPq1G@yp~u797Y<_ S]9iQ5!&Pv #x;wTH AZ)K 6b1GtQ~ҿG 52؅êS'[(V%KPh۶moklov:_,K3f *_ Η)g;qJC;}Ș!=λįaMCSё#GnX}(+ڥ݁*D;'+[#$Q5`EBR)o s駫}=WB@$?aXxyfr;\3KeE_,XhD\#뿨_{ޭr <-_ nw6|* b<9a0O5#-^|eIX`nխjR=^hqKva.]Ν:uҥKnt܉^ze~J{]WK)o͟?i4D$6X5y LI$̙3|*w3gNSyĮq@ׇ0:wr^WqDވO ={j`$ːtS4ݟWXy 5! \rrR«_ڷg?5j| w}j>_S vAvOKy #g.C}ܫ%UT!ga7$)szS~ky~=i*p5@ss#G&H]7!`GB;Z"B" " k ~en5b O蝗B/-A0F t_@E/M:qDv 1~,om-rrR^p;׭'iB@! E@םu>7B@! B@܂Dx݂'E$B@! E@םu>7B@! B@܂Dx݂'E$B@! E@םu>7B@! B@܂Dx݂'E$B@! E@םu>7B@! B@܂Dx݂'E$B@! E@םu>7B@! B@܂Dx̙satQQt5O (UTnl>lvnΝ|Aʒ93|Q:ڢȑv}ngΜ/Cҥ4iҥKn)yd9s~:=z5M;$IBYf%aΝ~nիWCdɒQ,L2%e̘QG~_>Wt),{͂sedB@! nDxQ bߠAC("g 3X-X9r$NIԧ2eJS`r_¨GsO>=Ԡ *Z߽{M8ɛmAᑈݜYܹ_ЦM5jԠʕХQ4x%*zV (Hm۶(NӷO_ᐶ!tblX@/^t~u+[.B;TOҰaC-cW ,@۴\>}P%qF&O+6cK.={h h~})e\;>}vA~-A "B@! 4^D$,hNZq~}aLUd|9-[L)]۶e P!:wh?-y!}ʙ;Y(Yy«dÆ s`ԦMe FBmɖ-u֍X՗[9h`^j/EA|uڕҦNC۶o>VC 4[eNJbKcm;u,RΝ=GcǍ%:I\ ®ra_tZL}9 BѺ@uE۷P^]݈#lL2ѫM)'K4/\veĉ4ԇ*v+*ij‹ӯ_?WT5SUȸn={iD+yJZx_^?ۏS.%M6̝=.+B@! w7^UB]6k0)[Al ay^J :-6ۚD^]!1&P)O:u$IҥKiŊ-5XUiP,ڱK2WF3m̙}v[߰ѳGʔ9Dr(EyX.I>8.DQX-u:B@! &PYxs /5i ]8ۺ4`W,Bxu֝ɖv]bpYf}nwXx`W*JwJv*}P[ XZ I9^,^OժU5Jūjt Cxհ5\ udvf̘Qyotא,'cLkbW]et1.B@! n"yLIiy/?J.pLoD9`9evoPa4|YZ\҂q^ WCv۲u G.LyXl޴*z,Ȗ6lIHst9^]Um *&d1vQGs Ï?`'9n+pTC%26|FMMp Qr7MdoԪU+ڶmA4תS: WCjfyDdvQ?~J+B@!pg-^N9ZE͞=E[]K!.W{:C?ӛN:c =PK8a[սq W _jS4y^w.k }6%m 믿R{D*rc}~RL)eȘ2ׄ9^.+?~:{M^&YB@! nsjGG='$K>[C0-^tYf״YA=\z֭]GK-eR \/1fէs4L}b.Rڵ},0/_D3p5̮,L a hhO4`R/Pȷ sެs&v 9O>:cV xo8|pjҫ^U0PYt e@9ѰaC~H'Oy?c93h]z ^U]/Sx&B=>|Y/~reJ>=A/p.{V%;y{ .\.=5kV޽fywysU/KتNYY㕂] tY؍/bϺ|CM! B#PxzM5ZNV{]҂ h#G RYHwKDs0L6-&;̬~YЕU/-$% sR "knm儠}ާx-R5oFgΜUA6POfz1O ui~.ן/ =eosňG}֭ T:e_hKaZV<ʠlr h[TZGZz. j)REj;-^ɫLG! B@%]*!x_YrEAy%Ăxr2%p/M(͹| WGfoM:r^̌9^PLE5RE@iBpnxGw5<:z(A a^Wɒ%8~EJ, H4$IR%1kIDl/FcnjQ9 qЗ(RkPmQKJ,^~! B@w"ܹܫLX88C/]R3dLO9<"¤I9![YCaeW*Q!{^DMlޢ9U qDCRJt_հSjNnDNBgΝfKʂcK.UQߏkpدð'IL%_EHaf1e8VgW?~7$,V/;/XeZLWÚ%f~}9l/_dB@! ;^\LTs d.D[pes{#’<۰$Wx!5xȓ' Q0< ~@ ǗCOr̆8H9qt-KÄeF`FX񲞾`NF`F`F3EϴcY,F`F`FX񲞾`NF`F`F3EϴcY,F`F`FX񲞾`NF`F`F3EϴcY,F`F`FX񲞾`NF`F`F3EϴcY,F`F`FX񲞾`NF`F`F3EϴcY,F`F`FX񲞾`NF`F`F3EϴcY,F`F`FX񲞾`NF`F`F3EϴcY,F`ptrwr% y=`TXfxM.ϧ<>znHX5szY*,URƉQ(tAq ҝ.)2pgM~QmHy/W,k^7Qa(P2Jw MJL3"w1w@,2@rKO]o=ԇ4y-ܻ PsFJi\1>`|iHdȥ|;{[h>:r6xMcιa^Tb.)79!?uMAnBp߆ml!GL=7"Ix ,a=!+$h|U'-t<\q|9 zlIry,iSzrx9M&M@ܰ ,*UJt &Me|מ L]&.\پ@kD E- 4+L3L^V'] ~NXNR8tZH^v3H]@ \? MGC%n֠3[Y"$zv}?nX"VL6-89I S%*}dH׷BUsED^;y_ Oq` ?E m&'2%ĔL|)UI:!B ,>YBg$߰?AL T8l,jgfKa1SC"jf> 7? ٝQw#/'yY믎>`&U#w')? )U'}eT*Oq?a@P%=:*c_|?p|~x w<%tiCBvdyJq+8y cqv_ 7Ҧq=w1C=Ш1Zԩ!C2t34+L3do}mo߽~/i?%] ƫ{Rx7H.!wC f8yMS:ҕ"3\"][,Nb@EP:yfxR6YUy賬)8$On<GExm9A@I6aa? SQ;:@:yZw6*v.#x=\bGf](_-'ߴ)Pk8!Po!LjdJRUU2+6/ }6d) f?'9e+&O_>0F 6])/3|Ӛo\;ql"dqmЯ"xׇwBb$=Sǰ~N)^Ď =<e~(-jG[4o$>7 OFA8z`PE^xԯݷG]>~R'ZZItkЯ]{ k=RRhX4z}=>ႎv p9H .6ȓtA3.tI5*^b| xOJ>gM#ևC\}E%J Njtn P[MbyIK"_Xȇ/Gh_ҢG"xy_tZϪyNsq?suYcZ\?}YO_g9uG|= U9A9 );8& n A}EYc,CdbM.K+,o19\Ѣ"N.'ibX.>twq!H?l\"RA"5w~-ߺA3un_V05׺tu#:ӘNOcx)jsoRg~JJĸ]Llmmźwu&Q՛yͨV>xIa 3yJh:Vg\e&6 ~keժ0BpOxף|g~oUxhU)N*-]|5''~(g1`ޗt*ThH=b!/TQQӒ((l? Z9Hx–YԲqM[xx;$N.a2.*J7$H #Yr5@"k%/y468V(T(ߺ(X*9LKڋiZQ~@-nb`K%k"c YȢ5xѣ)_ UNU6ȗP&Qó(Q7yN_'jw++J7/e6ب4i]s&'>fALI1jBЋHGykoi*􅚾}-R1Q 2SXTu%VYּ|VG{ɂ5+^kb[1OQZ=m2ޠ/eZORƼFJ'd\&s{Sӭ)mF@\ӊU/JŌ+dduFz3-zKg,V=#Vn Q1ҽ]Tu%/!?e|*VMkq$in+' k:U(o^7EJGGMzg*SreѵeKѠZ59chi]%x%;+G,acd"*O\Ҥ..!᳆3aU)eJngNm?Ԩ):6mk>o"iZ}8'sk*| Y%{n{ PYCKWTfi|j;.(`IZH>#<݂A2G\L|^> I!RZq`Q<\.=vl qp'{~H2/ `.Dq<o0J66.RN3hNʽK]볁=[a俏.#l o0GwvCݖZ:]It/?ohj4{ å÷79eH9iP_MB"+[@xx8 @3=^S% .ѿ(ΐpJĀ#/t#\>|ԈNtGAc#-k:-~Vɜ7d"A^@Z? 0pu+Z&ׅ M ] c>< iZ<ӛm&| GP>?\P QʧF5[D=& P;d cvyG:x 9Oc0W%S?&$xU> x|%j8 c/ctq8GGWkx{\YRZrYLL%H<VqI0SIYhɘ~ ZtLHfap #/z] oS'|-cY-XJN9-(O0ރkL YtߨXͱӌ] pxSLl2|uS#QƑc>$`r|Rr;CQmUi &E)yszm!,085(@K%JRt:>}\}h(n ]d 7*r$ѳg!;to RL ׯ.G_kcX׈QDC. ̙TJ߻@bŠY:aڵmX}b}ջ7͝Iϝr@]VY*2Ms.] h1h^ϮIVmw f !{n;ĸoEԕQp.TZK [?(;򨭂 e~r j pLll#ҐPտՇ ň򗽘,td*תtus™ yx%e!e[͘2Fł"~_pBj#кoNnaK˒#ۙAK1E߿lgyH5~t^FA}51mb^B(e2K/FTs }$K*VҲ&,O덦mp#1Ъ;R>+N$tїHL/K-OPNj^ TZ,ޏ:"P; gj)Y|wUʧXԠY`*D0M;E}?)fz>t)Ē#3eW^Kb٣֒.<#*+4-,yC2J7uc~+4-С4*vcĺ'gS'=?}8>#ƭ:~-5> ĨW]E |kNZjj/]/|]3*,)ޝ9+>U˔AkNxȝ]9/-WXW,j4(n.-Kgɓ.]h\ -{5G>#eyd;$J! vA;{F<;zŋeY3HfXɧ't4tEk) R2dv:U[L^)#25YuH> ѪtEm boiݙtĈE/7H5`$"ѡBItMA3@Fc^x黚4aRٍ^׷1qធ$\G-3L[k@GH1I,)gg4eHqk׫Nԉ~% . \8_Ђ$9!+CF bh׬_g GUwOtS$UXAU>;3$'V>m"_>D)D%M1C{1doǀ:a3"|D "3Fh<5b&/xM;cw{{D~gz!OJx2Ƥ|,?k/!?ܴ{v@IDAT768'Fٌ9%t?h2Oi]gG>i %!}PqoAc3*$3)\^^a|oĴ)cZOBAdJyj(# 0{7q5uPeAޑuke1c U|[*l 2Ot TE'k\ {RxRdHo~-IrtfVM>U:uKuvkd=7{.yRkZɓ2\;hY4g7W3L‘ŋ 45lkC>Ff6+:kB\=2PnTڄ8ɔU~W6*Uä-]Wd=#) Z"펤Kc[bRg }adm>Eۤ(Ѥ9_Y_vA i*2&6ƦkCr>@WR\_Xyt=RK3Me+\=\km%촔^_$ɾaP;YGT7?&Bif{RB  Sl/! ?TQ{,)Dz(VZbL Y] %-y Їs*/aM/uYBtwtgDz+iT>Il1|1r+ 9Oa7ϚeSq޵o;O7R`0гd:U:A I?]|-ǶSi\*lh)nZ(<)-uוK<"!]:rjy#CٛgŐXϏOS~Y_l%E+P ԪRR YçVV9`T] ̿(/=$EDu)쏖e:cfICiѼa }MV9MNwgUs{tеN?R?|]@^KZJNCh* f=dta r!"״h&JZGb|ٗF))dQ Os­8)$O%\X=>\IYk6jѰp|FE.h+H>ȇl?~_ޚsrz'$5MlY@n4ʋt݃'2GcܖI.iґk*ܣUQ#桻k1d%^+::<>O„ӚƧߧpT,^J@ aA-WJ=G)-.\(t13N??)u%N+K@߷|o1'_sCs|TY!zPڧ"i/2$cwwrc]5\)>m|)5cK8 +{KЗ/'"MDW60l?NoK8[@c.$MH/jȥ/R>՚b']ѕk=rAdBS[SCw:7*Y+7 (mhnWCTՐ-ZF p 5P~e:>.ߗr%Tp2F54P2Pm+s͛oKfq [(MsR Ybj(Xʞ=J W>v2kHiQt9~ηQQݻ: #vȏʖ]L:TGΧ I!IV? ūr^nV_MiW?RLT=sd% #>YLSMLz?&+`oKе.OҺE|bDE)#ё"؍! st5eݒk.|%T9>^EJ}W6E#6J\qFDOֿ/'7׍}>s5@p-1z(:B뽈&Wj}Z LkDK\bKp"D|F5'U3|1?z[LOŹbLd} SB!)~Y|q1W]H~k5wxjȀw_fO +?'ZfNw,>(H:gg*4>=/U:xVxRojӠPUwLjӫA9s5<8}\m Tf\E5q0ܵۋ˗[1iGeO? յxV=1Tv99ߑ FueZim~YK3SThHHr:mͬƴZA'*5uq0*CfHuI:).Zte}tZ|$VjLĴ ŋ^d/}qݴ Nu}ۥl& tn\Sv_h;[{!L5*(TPR^GEڥ֡Êj:|G'wvH>*W"V9tC% գJS=_tGYVq 0Gc iR>QĈ=h5bu)6ūP?z~>tNA|(o^{P胏qyZz@y&H =Դ>$klbRw|SV> XTyx%FWi2R '-;䂨iG.-^Xw>]ݧ&iaD2$ mvo TXڰtmKY.\ )),HDö2}hAr7 OKUk}æŊ\}!O@XjQCTJ̯K:/ҠC6tjIuT\ m!WCz+^+cg]Վhin]8 U4)$- h~CIN )d4sNk%׋-*nO-%UM}tEnK1z:U> TCn?MKU\ϸZg0>G2Z|]OKebk\9붚kgg?ͻljl毞 ҇#0<eDY(JJ}`{ "DGTc< :*龋9>}OKWh*~T/rUq54xQYZEJEԢ.]YE}bUGL-emqbR(=Yy,)ǡU4d=ʢ,SP!Y׃uSӦΩSP҆΋޺<'?3#\Sy̏ڊx`jH&IbP%For=$ţӒ=2-*kE"{R+Rt $rQv2ɏKG]*侘2ahG͒mQRoGt6m MHsC\-bxQ4:^r-^j=YԢ.F3\xps󢯜s𫥖ɇ{!rrjp֮Ą&'b'Wt+xveʄG[$᤮`Sݜ|>3ϒFZXR5\Fr"ZȖctKs_%|@HSsG:r[;k_y`Ce-DOHSɇM 5Q8ToBAؒqu֯/)$&{Su>sSN63Ir_lKtu|sj/Zʋ:>=?"/mWCx+!E3|uTSӳSN=>Sbb);aqϒ%"-*?jtڢ\)$ Ql3PPWxX ,9 ^?{cW-h1:v'_]Rs_AB:>!z.h5P>G |P (_H|o%{ԯ8 ngbsQRdО"_c$>lH_뢑 GU`h<@<}>*ܽ7h3)/jbs[OE!s>xt) $Ƨ|jϯg8ϵ0Ȑ= x.=b}[wu[|Pa`N9ou!|ӂy 0s[vҀ_=49Xʗ%+ޓ0*H7KGnrs hyD'l:e'wLj|ucU@s eV7>D/uPV ٶP)?݇BN4# UM3g-\s:<s2EsݺE靡ցGC`%qݻ`z% OMj׆GOO27 ϟv̟n3`ׁ4+U˔q3 ѻSl\~0y 8)Y ,ϳp-j>u}F'rHhɣ(^ I-'=T͛m1׿prqK!_oqq[ěe=<+ߦ_llmVV+Aٝ`pxz^NN1=t\ 2-,rLaCaO8mRgtC!]!)5NƐ >#:jҙ,qL;yQ(^%&\TjYM/gGOHƙDNaR%P}W³pXN2rߴ)6p~5UmIK&~O)Y ,A?Rl:7 T 9%FH*}- SR&K"%Fo?I;)ֆnrB"bC7`ZuV3Q_'?)Wc@qRʏ_RmqpH堃G=F 驝a~jqi}>O?[ /~|MQ(43Wm1<2'pr%Xt /&Ml^IB8NlO">eN /d/䦲-{o#A:]5hr{SC0n#AceY z|@Ǭr/fP L[cCœO_a3ICcݼpv A* C~ݨ$ʼѯez$C̙ r>5۷âLe Buޝt?1j1TjE:,Ou0z ,ڕ*->ە }a;Z`0X9i"fGzvZE8Bʕ`ɦM9bAr愠eժí 3!K||vؒ-:D#::z> *L.a? W/VU9=!<>7l%JWEB ݨ< ܰûrEKQ2J 7O;g *4T:'qE}*HiY< z!rK6 ~=7/K9Kdxu :<%^Djܽ J Q+Y1c*t2ݫxgRraRqhOm œVNtvCxںQ0eC+ Z|d SL M|Տ,e 1?!r\s*v8-[άYūWpz6Yʛ]|.p;gGy^\9Vm&ep<3OR3āx6muQ*^ހ:cF`F`F-Q*^\Cx]9`F`F`F Dx&W=[b.0#0#0#Dx!F`F`F`@wODG=q #0#0#|yDx}ypČ#0#0#$<x%<\##0#0#0e_0#0#0#r#0#0# |0#0#0@#Wc52#0#0#`+^p#0#0#0 +^ )0#0#0x#0#0#$<xE`r9KV'Wq8q\:-j4sqO\'!hi@0n_lB@L/n[[[QMi^`)Kx蕮 W:8K>gA]Yk8I:9+ Jd_ýޥpnUx5g|([^1eΛS%㾎3SL0#`iӠFr.d^&M΃&7px6nqS.I ƵZI׵E =qm5ӦBu)~BgQN#"[GY N)ҦG&T<7weYFSme|$pu/MDT'Eu]R²ǣ{kyD '-:hU~,NRD py.7+P x`Lq[ہs&'5R^$OI5|O [?՗zLS& lQmn€eFSA`СЯ];G5yJg77p oHO5uJɓ%qsg3Hm0w(Tol]$_vmQdgQN4Kkר{RqnpL @ȝPzC o{$9eAC6~bmxx9N k5ͺ@jLp0e 3}IJll+ux EֱHRn#:Vs{l]sP ,=-x<Ҡ"WPӳ4͔<{2aRTl^.k Ȗ@|69g*~ک{b1^|}5d(y89.U(EʷQGL;gbN?^|11#0y<µhxy2l|J]ԁqs5#^菊Gy6i Q>T&OBd}ؿk BX{X%KfCeA.6ZL6TfWns烴g4KJQk1R^kh!E2P;p|vʺZW`y?ISzne'K4bȋ0EyI 9I#_i:6X5V*1:w-=}|@EExP.'&/l+Pxk+޷lf]eQ>dŧ/6r+H9n_;[x`#glAuY dμ9JT4O*\< -81c1JVki*rBS0X9WTo_2Vt -w%`Q>Z.~`Zdzīj%^*n|rߓ0RJ%RuuQ ^}@jx-VZ>k/' 7wH!<#A=up/TF_~x.܉s%qH_8yѝ꽹4 dem{|"Q+xt54*; SK?۷/o,[Y-`wY 1x'|l>"Ɨo\ o e _6HBɠ79#}W݁L<䀘:@xG(0.8A'>*k13L?1g1'K k@BG'T4RCE_&~ĵi!e*[;ɨGy2;pe:Wٸkfx9I ܳ*S h0g!1{cʗ%z4O/E뵔V~e3g%DV  Y:\7分ӟWTӎfi5Rm1`{ UQDO8Q0f[H9 3쭲}0ތwbKW9|ޤKF@-1kLIǀ:w>xA[\ūxby6N#>R?E'_b&ͽūw-ŔJ:- ӹ &-U.waӧ|^> IR>dꯟ>*@vþ.=7zSSn]9t9!Kbp~&4¤7`H6 7.UVN3h>K:ׁ!ȼB5VSW>{VlOFu2m]e~+'dkb͖@՜pB0\` La0po0NH{)`!SP[Yp@9]6Hm{og(X9<{)-dj~2 @0|MKVf2B@:XPe:RzW*fJa`hp2L &ABՠՄo@,@С;5H1Mh"5wlaɘW껯aKSh ?j{{(l'#$e|9` /xr |T~}X ܺ ;9C2eo2XU#дv<<Ⱥu ^~ GϞK7nBn dqF.-RUۢO?!45~xekȞbϼžQW>G b2 G֥XG8cOC\DZńZ\uZe h!e*j~Lvd;{K&q ً'_˲e[uוS(Ӭ T,s>L63f6)$Qx)G&* [׆/m&|kobqbc(Q'A^*%cZr˜Z,?B{,$i?)\QFn>t,Yf9 1_Z.DKSzUV?y1Im2QbOX=4.وjbΕ:k熏>rBh+)Cݥ, 1Y0 mbc\-o|MQٗXq7~ &A􂢨u`DGpA46A+iGƛ1 -^A8CUqDDӲ^=wk $c\#mŸ;d{pʼkh8Ju5 B]J6jo2ij17`+תTTOҌbJ,.O`5>]"N םɍ1{v_ o8&R>t3Z 玬Kq ujFSH5t8al6P%:LUS*_ۜ:Nhvb]BUs8I$.6"E.k*qj{GR(q]2d PȌ{fk^a@ Ĩ*V!Ї"P/(NFLWKd5^rrdK=b/81c ƀTP"ŋyz+PDE653e;gnvʝ~9E {j_?{C[@cQ|Z.Ɲz'+Yr54j8-8G`UѢ#FnoBDJRmrb6fIhTʐQrklU3W倫FVHH)d1ZabLŴ9Uj:%I* XhrJycw{"@;E>?%-CˬjܪT**)׫MVZsc<)^8Dk&f M |EQF5Dx)ma:; 'may5lmM%P&‡(T6T~뚽ƚ xd(YIW2Cy{|gMZglRy*ԒyI*m{,W 62) #K\&E\,yCCgn2u$QjAR8%*I%Sl5(n,#HtMKIGz%p Ax @ J\kMzmƄ_g+JY cRGbhQ _/ 0ĸ-J)dƗTЪja|QGHH 7r%P >2F<x $P,^81E)ܺTlxūgֲ'Oiӷ-Ͻl5k 0auF* ޽z^ec SZ:^5'| f`K8cA*[wR {oZ;MÙT N3ö }dAծ0:j uB Xֵ>\g x`J3 *_P9@7 6,% >p`ehpzeXf[:No?Ýh@A04>h<^uW;`m&k\hC눭BϽ:fry)Z}&2x5uFK %ෑ m]H<0V90Px5%ݤf Y:=װ%V|5󄢵<>xR0B |[$`3ްt;Lyc;Tk/?aZZ\n`/T vZ >|kֆq343 .^" -/6tN};e8BfWPɛ+` kq]}ZfDu>YZTW?5O=kF|(˓u )t闏9Ybu1uHfZ|MYe_L`IRH- a&M񤤣pIubH(6PFKZ-^ߴ)&-<'q9Ӛ6$Fkf>d5RdMV+B/?DF'(=YpXG%t|4L|430. qkK+0&Nxf;S 0xib>iolxI\ H{W [uLk24czRh"\M8+ws,Ƽ1]T׵z{I>Z3T*ׄ'Jo([{"Wq{T6åLʺ 1S\b+ZHv)^9 )A9hBI2hq#^P9u]4yhbn M\h8Oq]j^پ"YrXcj\_b\w'_A2Pr LRGU>F*^><(?W*Nv$\]EV̮}m6\~t|dʙހ'2JFQHAV>Ui0h[J.j5 m>?Fy?chmZ'JԍYJz`W)M?ʧş28ׄnelkn.%h6PզTBϝ5QdDK6T>R8yiiՐEūp%\#ZiGrfɢb"&Q(!շpɑG8q|x ~V0#̠ܲ Aq p7X Ep}zx!L~؀6.@afEW9h?k\K ʔ0 =£kO.a^ #rA0MJ0n_ge~h-)G{cpdh‹k`  >jRoD~)1<<6}0 I%1Jpj $?=4K@w;琧3AVE KZ ,9^?SUIG{G;(R3Wp=x#fK 9fw*E=|e0`F8{廊|P cCz_;a~sE8`F @ B`p |Wk1F:Ðxw76]7@O?ByA؏?k//FڊZ%*^4%0Pƙ '`\k&zN.[6FՕ{loӥ *ɰZV*ٶ~8{v:? ޹a@ hh7d[PN}xJ'޶ʤzXPy(88DH;˲׏[vGWMh*A*^Jjnr=T"WeM5{Z'">}^G7L &J{j%䃕#v3#(aqV5NjX|tڵui'ÆC|R@+wзm[jŗ2B4QzxU xhͰgoG @Jgt ?-i4ΐ.kN@>RB\h5K_yT%H>({x`F`F`U RZNϊL0#0#0@Dcj5HWC?F`F`F`#eɝ0]Xt}L0#0#0#`F`F`FH4x%Z\1#0#0#0_x}A͢2#0#0e`2s#0#0#!,*#0#0#XV,;0#0#0_x}A͢2#0#0e`2s#0#0#!,*#0#0#XV∻3L2Z{ʤI۷S]CrȐ2\]]s}jf^<CKF>~=2ˊʗA'_) ]t,KrkT߿O?PF*r!XԩիWo^DIR9zΝ ve^X/9sf߯҄ b}J 3g5ct*OlloQ9{40^(_G0Ld (IDAT2Y>4w(_-7- mڴ!apҥFgYµ+W`޼y2 RpHUgϟsaǎRT0#0#$58[ᓺO=E*^ʂk׮5PjT$?ؽ{ b޼ycQ1٠rxA*[-LYPhZp!ܸqC=Tw!("/!mڴ%^(Ш=U$kה)S̲;Wn肊۷P>/E'`f2e իW#/oTS;w >GTȪ"-/PQ[t)cF`F`,` ^>67kJWP X_@ULM8WdR1U*;*BOhҧOm۶ ћϬ%MGU"ehU/=UGxMP רXz)EFw۬u6ZjPv,^$hJo IgggKFA2{8q8?F`F`FGUWC}KtUx5*&^2KӤ"DԷo_]w5mAtEOmٞ-zRҲxEAWCR,uT1b 8w CR2駏9)^xm5R U uՃ׈`F`FH X#͚ūdIv-H1V'!kF &M2K{)BP"mEԪUKݽ{~JOioڵi -^j^͚5e0Z&kXHn]p Zx:8\9|1{гW/ pxEX5^ڊ?`t0uTx~|0#0#$ x\Tpr,] "tI\lw ] WîjQ@EX]TBwwwӧCѣFq:*Q} m(=R*^Qאu5l/Z>~q o+U+,d\\.cWCE>ɜ6k,u떚GF`F`F `+P7GWR0^jի3gQ: &rTCǫQ ) >A?U"9WC*Ck||}q˗/)9N?ESs5Juڬqū r1Y0Fm5^/5^m~l-)Su%*~Ĉ<ݻhkz5PJn1\ ^rehРt1$-#0#0#GհtpmXAMDِ'!2ڄԀӧOUGϞ=^KbdёQ2!r5RpuẂׯKڸ(^JT(ۛ0%s9|,^'ߥ Z*[Á2WByt2FL\ Yϛ7/tΞ= o\HN/ b3{5  Ȇ qiF`F` @sy[(FNjT-\ E[y g-Z/^UZN[~[rnݺpM Wnj -^x/_c52zHR*ǎӵۓHMٞ_=,z]3|>eEoc*86.HMH@?k)Fkkt (EdE⺨C#je,.P4P?0&Q%Ej1a+JbPh#}snϥGM=IϹ}=79<}jF#ZlׂGeƛo (W05w2^Y]3f7s׎kVl<ٞHHHH^zɻ)瘝tQoZK)/oH^\ (ǔ'>6Q(a[ZZ&ƫXe4^ud֬ilo.f>(m2^5l͕{}Yw^ikkEڼ-/-p Zx<',Ǧ&) ay˷f'߁p/1weZ[[!&~o赕JeRL5e0}0ǏϱNlbNNY-gN]^ aGHy= @j^rwUˋǴfvA11?SȐɰ`X #``Ļ;o0b,:ݘsփ~شnz@D"ҚR;]MQaS>lG=|ԛind.\#h'XM,Bۼכ. o4is/^W)!ZdTTT)ds jʾ}\b]R;y_컈)31VjY*KC'?-3s-vםX1Xn'   7tx}?~m4mz^k۳gȑ#֯ԵP*0, vpX?h'\xy7I!¹.IvG2^ſ9R. Q" 4j*yY755a2^N5ĖlդI|K>.Z@OGW`(N~&4blr^~Nu= HHHHFWD涧S }5ʭZs`KMcߺuR]߫aI0UfJKKe6-5y#PsxȕzBk1'MM ^9992{l'OZVMߨ#]7Ր}/ O+"cWǫS ]֮a:N6^aw5>c%#@BDU !kt[Vq H-e t);S4 Ivµb0</+SN~\ߖiW7_U6kLt{y631;`'ȹ2k,j 'pD.WAc^N^9 {/'<"ܓ tT_IENDB`golang-github-olekukonko-ll-0.0.9/_example/hello.txt000066400000000000000000000000131504320775200224700ustar00rootroot00000000000000hello worldgolang-github-olekukonko-ll-0.0.9/_example/stack.png000066400000000000000000003720661504320775200224630ustar00rootroot00000000000000PNG  IHDR|B2B KiCCPICC ProfileHWXS[R!D@JM@J-"JHcBP ]D aWZ *+b tW77wϙ9w;Ri @$O"uh0_ r,˻QZEK( @ NoHey7'UȠW)q 7)q _鳉B:/@|AԡhD(@Ond!s!6pNRN45Ari]rssXê) Q ${rCA 6(.+13SGmr.`BV. ߙ+;)Eyq!\aO>a,XH‰Dž"b"I|y14'+y3cTEҼ8xy?4J.,5 LY@UT=Ad C?30"G @>axS]@zR%<8xS z@F ` 9*=?~g8 g3@b1D p W?Xq61w{SBpgP6˱'VPǽ:Tƙp]< Ynʬh-PʼnRQ(6CGji(sc~T ;3t~6l%;Nb&XւUo O6f?Ye&N5NN_T}yiyȝ,.gd8!b$,g'gWMtwa|#߹s96p@!WqBo:}3p^P @τ\`(%`9X&T`?M$8 .+ WOx;ABC>bX"3F|@$AT$ d&2)AV"هFN"6D^#P UGuP# Qơ t Z.@eh% COh;11Scc\,K16+JJk֎uaq"Y\!x<.%x9^kCF O0AJ("AxG$Dk;܋I,  b1D"IޤHG*"#"']%u>&dgr9,!K;Wȟ)K'%"L,l4R.S:(ZTk75EG-RPQߨyE檕U;P:W=E]T} ;oh4͏Lˣ-UN>h045xB9uW5^)tK:>^@/_wiR44|ٚ5oih1FiEjj-کuA6IJ;P[@{) a2m3O'KDgNNn4 ݣLi1s˘7  [[ W ;Eޢg+ӟgxg,sWY!YgGfIٓKM=,іdKNO6^5vUjoLZsԥtZZu떯RY~¿bz׿ puMFJ6},|{KJҭĭ[nKv/ lC*t{uNÝjEM箔]Wvnuݲd/ثǾ}7o>>P{CCuHáq<ɴeǨ=^pDɌ'5=5ѧ[τ96ss{oyEKnZ\[Vֺx\ilvՓλ~Fč7oJ~[x;~ݹk/}`w}Q죻_w6/WKWW枨r}~_ACGs?=< KWۯ¾e_ (6- <7Rǫ·}QiOXu+n.ws hyt:p;w*  6GMMΤ?=JU0w߃?SbeXIfMM*>F(iNx|BASCIIScreenshotF] pHYs%%IR$iTXtXML:com.adobe.xmp 578 2172 Screenshot #FiDOT!(!!h!p@IDATxer.{#@%"A* )J)4)zwgw2;7ffowsi|gvvn-q[        7|;        -@        g|                @ g;"       |p        y&@G0       1       yè.        @        g|                @ g;"       |p        y&@G0        W\$:$IfV,;Tt#u-5ͭ']Υ2XjeQ]|FY,?^K=ALɢbݳgO;vl4Ȍ3dժUI1aǬ@@@@@@@Q'fv8wQ̲7&ʻL3#M--p&yc:yzr~r`nhsCc~:uj\d]13@@@@@@@؆R vKJƝcs=I6w0       ۪@Jloc&V74ȹfZd V/޳ZظjiÝb@9ʥZ#sj7W/.ke51Ö,d+oSO2oLC@@@@@@@ hw0RڭlL631YE.ەux'{UɏG˼kyyHwXN:$9rlܸQ>CX+:\__/WYf{g'       y%ՀԡbeL*f^ c V~0c.Fw$7v=푥1ͺ^فh|i\O23gΔ &*J?~a1cFy:aw (++?}tynG>+?^}W_ɣ>j.D@@@@@@@rT kErNccG(,>7k劙>Fu*m?-A>,7tZʽX&CWph͓o/:u˫[oUKz+B:wl/7gemDWTTӿ _bǽmե+tsl2k+'iԩu"!       O)|6oce}ݴ6'j!Z=| 7Y3Jӿhs{ȇڒehw&{K/iOSV>>l]W_^xAԵkW4iKKKkl|s9N&ӦM[򨭭?|p ?w}̞={       @ h3fnfk بoiTtu(;HKM-͜'ӫkqOQ|YTT$ƍ[X`Owwa.줓NjFӳ>+{\}vK֭_Zk3ϴCyGaad[ZUkԨQr%}y[-@@@@@@@rU ˿#.1V74e6y @J:t]dpyu Z{# Ж-V50ah'>n6),, 6[/((#FӧN*ēl|ϙ3G.SSdw21       @fFxa6V74ȹfًuX(7ZJn]646ʅϖfUUGnTN*#*' #EA@@@@@@@ u6 j]<ӻ]WW/#H|ߍjÉQ|YFMڊ}K|ēa:       <_qqQұZjƛ|/VKJd^m|˹.6wIВe䲕1˄-?feY q-HQQ̚5KP[Sު ʞC=j&        mW$tMD^^OT8z\^&䬩3Z +zhubysͺe–, W^l2Cm;>(o-?oUބ sα /Z        @V>Fv*m?R ,E6ɤ/IpJIV1Gm:6+ O@ т/r̩SWVZZ*'|9Reʔ);|8lǤId̘1v|AObxgnfO{E&C9D;8{| =XE}O#       @JU.۱^&3?Q.Cdr;s{ep`Է4SU+ 5ҹ@C&Tl.{fu\=sx; [7,'p3I>niT FAAlr~m3gt\2t^sse… n\r%MV+_}TVV,?w\b we9s2 ŋKǎe]wÇ۫_|zRo$@@@@@@@E drE1% ح|0;̍5rլyו]8зWz#uVµVu]&lݙA7tZʙ@؀|h`Go3[v} zH>0@@@@@@@\$]_[ݦS ]#d+g̕Y56Y{TD+gQ$M--نEUοeBgpQVV&-VӦMϩO&|rEk,/o5u V)Y&ծi #myC[ і6_g}=)f8h_tLmD`Ebb e@@@@@@@ \nEҿX[dѦ:o'.WRF X3f[N.]R\Zwҿ  Xre.U/i]4F]ː@@@@@@@Wv;z#       ^>"#       9.@G        x0       9         +8        |z        W        @ ;!       ^>"#       9.@G        x0       9         +8        |z        W        @ ;!       ^>"#       9.@G        x0       9        H1\:l73~˼2}c3@t +]ᦦ&9       ۶@ҀCzt|d|tCv ٯGMs,Ռb@@@@@@@@ |dHe+dK"#m22        .xijYX5xΎF{L%%rl̮usqcA<N՞[RYOu`eҿY@1]:0       @H;36ڻ[h_](npsq ]1tܳ=       >2 *D@@@@@@@G">dM        Z ;tcz]:I"\AEV7Һz5`S]L)}{9zw˜Vw2K6;:P$^\3͌ *)ct%ҧHZZdE]̭$-_-RU.2sg)P UVZ~n*Y:*JK夾=Zyj*go.]T       @XvVF㪶XsZ!,],Kyax*MVlj|j‚Vt{1nJgJw+/47Ƀɋ+N%cɎe]|w_"^ 4@@@@@@@ڟ@>qZp' mbyc:{Ѩ>]& .wX>rTZX X= ݓ[ kϾZ S7T;|ڣ\6lYx|ēa:       K #1~HG aյvku* zi"{\:9K+:;~FV5gX1rR;*UMU4+B9W7٭kcθ\=|н™ݸf%LYQ.#;wrͭ˾' ?8J*|hr+kge>\ "       Ў2G2:I<̴czww64,wrш!fT~5wL^VD~3fӚȧV#Era_m 81{p1 ѴQΝ6a (-QѠΔMxIQ{?OW.s Hy       |h+wʺ:ym:і05HU}<:H:K;t[;͘a2Ȫ/VC56V4&|yq?(}mmoV+!˶t /c*-c;&-}1 )g&!       2Z轥 pSL]W-XR76yq|9fXi'(|ˀ7c.ۍpcyuZg<ـ7Co|okJK`@@@@@@@+@%~ZaqպiûdЀ3w->/Z HL@@@@@@@ڍ@>TpTR9wwWE xsZu_ Sn;iMccSfXk9}s@]\dd#C E+¿|"       |+ _R,w-#@N E56ٟt/  hs֯͞/ovuۏV@&oǘ-#LWSWQ{Q ,)[(/k`o.^JZ!;uG[7WٙhD:C@@@@@@@d$cHՊE'a~rTf9/ֳܫL~tY{7A;}n0gIG\f        Ю2𡂧% j'ĴtVW(@ 7CjP٩,jcMc ﴹ0q@9j $QҼ?U]$ .c(gƘtӜf_&"       ׊Uz{zY^uܙa `jfu2S{=dHLZxjeNVƗu31XQVX(#.څ/7SV[B|hzu.jFt.qaڍV +{dV0?1Dbkw_EFwjwhn2Fnp/0       >.?Z%QܡRԱJ*+@d0ANښ`}εKA#ìu(*{ M\kj4Sa       CM>e[IG@@@@@@@mA i>V# w[;w̪;.Ov(]f.`|d"       lsI>-f@@@@@@@@< #w G@@@@@@@'@Gl1       y>       ?>>g@@@@@@@@ H@@@@@@@@ 9[       y.@G@       Ob@@@@@@@s>|R}@@@@@@@h|}#       @ ;#       @ s@@@@@@@\<߁T@@@@@@@ڟo        |        h-F@@@@@@@< #w G@@@@@@@'@Gl1       y>9-0d=U?2|f@Hw݀zYq_}1@@@@@@R #5's=tx)]|%ӦG ƎAǝ{TdY .+=vhwE@@@@@@|#k8gtߩ]d0='Zu}q`rg<Ձ].%Uo% OvƷՁ)?p1: ㏤~Jئ/sw0)٠IqZpX@@@@@@@ #FȠSNsZԓ2w3@H%RP y}IfZ;NV>>TU l o#w7      "@G*J,5>tZEʻn8hn6E{#}p+?ܞ/}|Q;@@@@@@ #Yq nU{lXPqp(h#>b@@@@@@ڍV!"=vJ7Wgĝv^UIӧ֤LJv/ŽzIIVw$RzlZXj7_qt {^Ú5RZ]|tJ6M@ jk}סbe}uVRj3C{խ\!M}dt!]V9on*g\dL0~3=RR:v=f~KƤ)SDhh)9J KJvr:UrYS- R{NIӦMRt-]"`HcoMGKK,{-i#+zG7@aZ8~#8鶘8ࣸXvMnj dg.@N|"      =ұt?)=iYRh=4?]{Aֲ%JY.t98;ߦ5eK?WYa R dsM/qvYN5V˪<`|7bM6A o%f㎗cY'Iΰ "gWeZnkJ'8˺ZmxRXT o\H<+'˲RX()&>?K(_wR(_e hG~謮Qj       𑡝[޾~| .O=-[V+5Xj̛z[C""dݓwz͍ q+׵xҡI]V>XDulk[כ?k'x6aqeGCt%4g8qfVZf0|"      .&cԨQRnu,2f *O!iz?Ŵ9l9C{nc HIE7eBmխH37ŝ:][X7{XZt9*,ςN#VK &)?w}X^Ҧ|PS-ٰp3A/L*uW3ѻtZuh`K.e.ci'jk-JIN*̛'-V.OzY]Tyt3<Pt0ǟ]|_K| 8HguOG@@@@@@*7&M1cd|?=2yPt!߹Y%eͣ8v :$gb9 -={ɐ _7+YSθx>*|]Lqq?|ՉzRd\{~>pw*i !NZ:ْ[lRO3-Oz hQPqSvѳ8bW ru|,3wzY53ϿP|uRȲlmy=_9-;;HQX]-VTK.      PN"Cg嬹W㏝K.u{> r r:ԫκuke__baTSnh|x}dmL|Ydw=#8'lA:-Ԥ[ yN## @@@@@@&>LquOl3+So_G#;7MRyם6XKA{zoN!$Qa+}Н~˅aag?%!      оtgjHN2+JAZHpL%CKaN>pw;>Ξ@㚒m0#R/lF~d4#˯:>0@@@@@@`|sKGO e̜!{o?@}3*c(t)EOCdg\|TeOk!TG[z!ԷפsuSj9ivbOAw^|xW)XҽYm?ܞ>Ba6;8߃L|=_O       #[\pWZݥ575ʲߒ>}^{Kux>\WO\_|KI R7B{ZuRY:&#t28i}{Ȑ /r{b:gɋ/H12ҦdwTJqhI:N4hW:[ӱ'dv!!a<~ sN RT$ EU+      +>2ˎ=NoCJkDH2W8M 7Q&e&Hvtֽ򓏥WqE}/w\|uis+#F:zMi}g= >]BVnrkilJg8 !C9حT-s[jj`*{<}.tnnZY/+`gY]f :T+=0eslggӥzTge:)ذj`Xt >P}:V 0[~=}T'gS},.O6l_ TIRԹ|kz./^e8 l|9{(ؗp^1E7lܼEe1u`#R:vL9DzwߑMyTVG/?߰/#hG 'J1Ŭrѣl       G r8p)Z(,*M~|#n]>[g,[M匥"M|jv؀kw6ݬ%iU7gU/p& #?1r#Um5%>6<]-}%iӘ]nFEɪtBV&ٚA>λl]5DG #      T6-ڽC䠩.=a߀kN4qݤ_w-MRtE~pqSTub )_WgzﵷtykYFW`nU.ze@~Rڻw>X=s~vJں\T"NFkwVJ}.zt~+أel r),.Iy}UVIUNW13gJƍ rh#Sǯ1A_buRos+FVS'k4i>KN;)b;̚){6Y6#      ]bu2Z07$`'0b9۵g`E҃VwF~jfՃb      (@GPKrzE)X*ɄdLWhY1      QbuTt3^֬eU"sc_'      |0@@@@@@@Y>rvP1@@@@@@@@_"       9+@G*        T@@@@@@@@ g&k_ˆ'˖-YT*       dR o>2@@@@@@@@| #uE@@@@@@@,>8 @@@@@@@@< #vE@@@@@@@@@@@@@@@L o>&N(^<        @4|DZ@@@@@@@@6 ͨ)@@f@IDAT@@@@@FhY         f|5!       #kA@ Ə/}Y^ՙ&5jTTTG}|a,'0@@@[_9@@hƑ ЮN9m^t|jh Olnn?ouGmH@IzZ~,[LO.-dSe~ݖ.ۆ m)@G[jgqƉ>ѣg}6|_"쳏/\s5IԿ k38C]YY)vX;cSCgc5(oz˙@|7# 't,fS~ao~3@ [=1">@*YfMtF5~͵=B}@@ _=fԮj窩?~kI}'J.+o?eԥoݻyKcc|;Ew}ewyG}ݸqyI߾}efnOt=(7,{؋555ߥI&%xW \m ,?я`9k׮[oUIvc~=\3f{i&YdL2E^{UJ9=M---6a6ѬzG'O7x#n~C9ԩS^胏.LJKKnwƴik*t電.\(==蟰/Ѻb^6K>}n95S6.]0z0߽Yhanl{~a XMz>aVrް)Q?^~[ovСCwz18-5u 2 O'[%  |މn/ 0Y>?4[NoE^yaSuu|k_K)O e(%9fΜiUqwu5N?dرv1Gy,^8EƬ?T&H.? ?uJY~_դݸ|#>TwÆ {g֛@@;裝{|d <+W%cn\|vz;Aכ7.5BonN]$Kaezۭmo{챇g?!CHb6Whk7[u҇p83O~Ytt{7a0u2m03nzt+ Op/M?l5szxW ݸ>^0SO9ׁ=PZAA(dl+?m1A^r%VSӳ.?ȅi_Ú@5>rmd>zN"3gx)7Ƶ+իWR(nاTP]~jm~"ۮd ˟ʋ"6|ēm?Zmꮛ {͟AM}K[ФZ/ҠK9)SetN #޳g\ꊊ nںv%w. {z,)_L:vSvoY&L|w}{U/N[ݰ/hFv8L5HVPt',"?o/XՖ]53)&ʺ](>>LoBeBi˿_ú@5>rmd>BoGiBGZ\ZeN}{ ݠi6٬CuZ!è;"3ta;tmr^&N3L;2N:I;0>lG+7>Nx m}ap7>hKڎ?hړO>1e mM=MT> &uMv=7]?t5P؀g}VFih5O?k`.s1.MWFΝtie9#W_}m\s5  գgGY-4͚5KN>bIleX?z>;#={7?a=?̸>` ~KuנZnT]-H%~OuU]Vr=e??ٮԿ?mkXw# &@G9ST7-~rEe&C!#ܑA߰_awom_q>ru@A9wҀ'ړM&gu{vawG84$m9OB]$jŴk6a+oZo. ,ϡ_}UWD]~zZ-K& pfA?Ow@<^O3g~֟>vvM7٭67pClg; 7v1Ք'  |^&޽{})SF[6Nǰy=]D}kIV6Eo\hh_ayo 7JeeI^oX·O?]@Mpow/|-K/շƍ'eeevjo+?fe!F-{v8z_obD$Jޠ0`V^rh/ϖ/_n>ovjڟ>!eVjqz %A-4Cmϗ~ǫok$֫W/췈Ma7ꯁ5zYvT|]t_"w/h獃:(β z*g9ꫯε6N[V}PӟT5Mr'*<'?Gk 﫹NqM|mfG?]X /ٮ׈dAߴ _4/iw2}J㏷]E `illmzXL >/Ǧ E=7u L [(?~~kV~@.w/O6e/l0KL=av8~Ü矰~ѬC_[9zjkf' r"Jexk>4s-tݿ[Z<tUm/aoo?fS)O@@ ὦJovSOw;=ևgk@`TӜ9s7Lsþڮ 2}S$&~x7㵉J@ވӛd_~]vM0AL[{Hovw%?>4H/}s6]uWLLzD@{7T&ᝬ7Ϡ x6>e],`}9|G v~p{A4-n¶m E01oZOҀ )}:~xyG߃ŋ;xxO:JN{fyZ ;i/@ϿtྐྵjGk7LQ{o@,ֱ rvwͺn?;o}u>kh KC-&zݡIuw:sv6j/Xm F5PG]/vוn;AIN{9O–U0Oy} Qa'_7a:z K sr~A_K[N [(Q|<32zhP'b{f?lmpGqF߯:0 9>T785Ci}HӥK-M_TQQa/Y>s 榷ߛZ0w's^[ Olh>TUU%vZ7>0w7ѳgOJvEg 1\O-1_^ڜ>ro4xCthkfv &&CMI}Uc֥/AB{[ս^[~Ͱ Szt8C-u.\]?.h/7f?AGylB¨_iK-ڇ;=~Ö v s<2{]_}NF9z}oح ڂҡjgӖ&\o=βfa>a?{q|5b:8?vf<ѧݩ>=߄HA?%}U,o|jKm Ѕ%9I[Y+-h B[k^?3=Z[j[ۍ7(zҤ1:kCڭvoڢ~ rh+l=yVEIb%CQ?Uwq&W \M\Ֆxg_[(?1Y^?q =C\:Ks^ne)޺x5z1LZz=Z̋jn{Mڅ`.lGQ~6}oxד_u`@'>rtooOEEEv>6K h>y=z$kܝ}^TiIoIߒ|1&75Cf(>і1MmMEAZ(7sM(7_I&C Q]=4 zcI=f>Is}Uv'ƒ<0Eդ78to >mR#}m󇫮O.c)OoA(ו/~ kD?z=F>hԠ}UsvuS&0-0a߰a=x>m@ MЧ#dr~mRz ߟl~4 WmO 0>t=mzq`8tn Rxs[cՀo'7 0A:4uw=ikq k9M5i73C O;GE;s y9- q?:r8aՠiŇg| ښyk>tտ⥨?fl{~{p?P Rz}qDSܿWU|Mvkrª3{=ɒmôҚ,_g?۫_No=կLC@ESeۇu6N`nv[0Y.R]N9#XSׇ$zsMfշkiɯIvwڿN 8餓2g=}o־:Mɂ^o:{b0wG<04k *sLۻN zQWM~o XcIn4}XnL|Z~?Z ߰~aNA(ו< KN8{, 緬>5-$| |f ~o@Es1Z[]ɣw)>_Wgh>fο6|߲bDm~ZKEUx7۠i 7\iw&{}Tn4*n޼϶_Tg(Ѻ$s- jE?_S7s';kwJ~w{ܭuczz2LQ|4N9rx޽m .r)e/AMz-hUGqzy ?/Nqhaz`˱cj1o x[0+A_+vc4 .-Uj˥z'u{O k 'M+W\IlFYǯv_exua:  |^Ҿ_sZew K/ߢL|+_[Л憾eJ5uT;fnq4@Mߨ>w_Gk8wAMy7b{O'oi`H߶5oܚ&½5Q|)lG-&I?h ;;w<7̂|_Wg|nWw|݁hh|h= 雹NJk7uD[ }T[c/%IM;bZ#zYFuQwL[Y-iKs8oo_ܩjh0h1ڔ;sQQ}̶kh7uhg.L9<~_~{-zM^z:~S=g/1d\?jOݛ5;鵃ȯdOuy{Q?q{WgOJ֭{h׾M qg3Q֠Q?7ߟD^5Q} 9쁕fe>0⁁aӒd17|<:C7o1;r's*H+л.d~y~oia_w_shyɾ?|d2A(ו?nMgڪCG&bڲ*(G3JGJRFA?a_~S~ {R pwzϟ/{*NKkJ-hh]L e~NJWvQ-mذAvygHB72i$;8;;j~3;E' 4._&='݀WVg0FQQ}ܛ-@:ͯK@m=mok?ҖL.a߲QoTol{~=~tme =m異dO:וmڂ&}ƍ'yu/+vQ$߯<@@ =d^b}[MYzh{CٻU%[s7d͔Tdn :<$>H߻0 IO>ĻʘӧM'.nvãMjꫯOtg2#Ha=psT/l;#2{]kw7ްrNuz=<mNY?tRoοA?7zTi;>Ig 1~A~?'@yJo?ٙ@7\v7Ex-ָ!O7mUD߀֤ǘyh.[/ӕhC}IM:iJ5 <Σ]yECIWTtmoLkg^z%+ݳ:7]*pӅ-k֬q:뿵&C} u ׏t;_5/=ϝSaJdi Gush@~O5鵉K? >ܞS{)QQ?}q=O6~M~, G~'LЪ&uƶ']>@d& Wm%AS`17LA zC?ۼG}kGI(>NW^  Mf͒O>ٻʤmD]Cm@Sa7m3ߟ-d so7e~v>͛㩬yANzQ\Ywߠ?'O\ng tm յe %%% 8q]S m]iK[4~ez5k@^^u+w/SN9Eme^js7I*++ů$w 1|h0‡&}Ps_S=OD1ӣs1&ԛ]-dzem閬,s~y ǯ_twrV>׽m\p6N!Z !B(NTL7`c{ۿ>]hggwgGs+-򇭿~<:>-ϥS}'d:<(8SWH&?;<<=0߯HHH >r4/raX(**I^ f073`v؞žfck QF:6loeۙq xJ޿{ZU0xK.#h#` O*7lذ^^+"̇_tEq٫23Oz3@Q)|` raE,&(viݭ*X ~֜# e<6h !%S3d2׎ûо,~;ԟז1# . S_]i?sW~@1XaNJ$G(9}ku}<;/q͵)60k,?ّ=skpɶ`[ek~H`GL!݄p+aMSәm?F€B{+SNF;/^[-mn߾bwK'V &g&)|W6[ &Q@LmڴI+qO;SCH 8D6gmΡSNYf\;FO\ˤO^+|o}G ZɬL-mYp^? <4`&0Fa;|wjJ_om?tF"mN9w}~5H$@$@$@H 9޽mQ1X88L2``)&1 S㶃2?OR(($]ba߻;a|˟'là+&::qPR|$SڶmMfbV==:XMl~Y2w%h0ˉ |L+o0-36;k!ߜ9s6/ZɺrJ/{2X)bNYċc{C3ΰoc&kqﴆ ?JG }QF}!qva7u~ jۙߠ={T%,x:nύGNî?(a&\]+ӳ>,կf!sQvtA'MP_,X ͚5LScPhla/ͪ y~߾V폝0'-q8H4P"Ν;{}e۹_eyiWmam PEJ0_ E`o懼/oL]/L[h~|zIHHHP#s;@=>B‡ < jtin֬Tm=D̀} cD 1q]w L*{0 ?%.C=x7}lz@XHMy>=.LR~M|aߟQwiTb(EfEzwI}t-+LɅa돽x֭['ۻvmZ9}>3x\6mZB*8>ʏ q_>d?Qll Lr hf̘^:{O[³PJG.݄+w?|v,>ywX?v^UCD=E?#+4~6XfWxEæ?EM4ڟߥ$ab1z{}u•-K6X?R~}C9Ƶx.6fT|(4`68(?߯~k$@$@$@$P(9mO8)|d2Jd P0iҤI̲y'.BS̀} cX_0.hr1&X̘;wVv)~Ύ'pn:ċӹ=zIL!:N:cbLK`)a 6Acz/f2‡,|` &&`H`& #_<0S`n,k:k᠞`KX#,Lm9ڵݒ<1jcUaǬ3i1]ͶQ_/ .평^IdXiB;h&MVÖtq{F?l,'`{ c!`[luQ'W?LAǀ=q EXyV"OlEd[Mf͚:c[b54k!KK zZƅI,3D}ٔWVEá݄;(@+gt?Ӷ}<>u?oX(W_gV(҂>/yD?8LR B6xfWӦvw~K¦?E*mgwHGaENX+͌w*`ہr.mQEEaXb/G:-ᰒ˲{ǎܘ\73I3F80C6^-[x ݻK,Heg,T_#8Ĭ+mtAڌOń׿(ʯ ";f*U(?eC mUCaLFL4Ӆi?Xe =ܓZ҅~.KA-"ކ|s k- s |z /~Cuf6p@(J z衺-ltK4]%r`O;6QT%͗{/tG;<@//f\/\/꿋{` G|NWwEklߤ+ɍ~M/ @&@<^L&_?,e$> DE ±ӫ @I`'XUJ, .ei\ߟSDk^{zf{챒,C.+YKJ Q_RlDC~nKt-a2Xt1l0yI$@r y` [xLnL~kHHHPÏJ]0aBѶh ,Ыva⑎HH2!I&e%u}&N:ilʐK:zv1D膻駟ξPy$eog=wO~57"   >l9x)|ڵKV^-3gΔ{'%H$@$@$}OW'P0 ;zwF)k׮uB ?&?7~ML?Gnύ} '@HHHHHHHHHHHHHHHB YHIHHHHHHHHHHHHHHH <*|gG$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$T vFJ$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@ P#<;$               :HÆ 5 &͛ @ GA1~               Tȕ$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$>c$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$++9A9HHHHHHHHHHHHHHHH *|HHHHHHHHHHHHHHHH Wy'{z!5kV0$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$PQ ‡;C@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@J  ;*|3d$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$PQ ‡;ìPQv}:˧7͊)g7?]c/+.gnnG$@$@$@$@$@$@$@$@$@~G%;Ji-霯+{..;"v5BϿN*5AåJr'el,&P巀Q3%@ o_Y IHHHHHHHH P#@rEI d2Sd5q))j[*l({vlճ?Uj5-zW`O;5HQ>IZpش*nO#ʩGl$$H,nSn,{wU3?4EHǣn&]c:)g׶H$ˏOlǟLp$/%$߿Ap.˟?WAAXRoZ:md)aٽskdXH+u[EeIw@g HzvڲsNY~XBlRj5nXVۻw,X e 6ի|7# .L        T=נáLy^l][@IDATX{$]ܡ Y8gx@>_mLiweI5v7IGucmxP٫rŕ$\Icr9rVؼJf}|o{jIvlY#>v| ?A*F{pˢ^DO<.8ɵBح,H $/Ņ*߿A}.˟?WAAXUοU?!9PԟCt{悫JJcoMKLkfYKۧҏ5kJe˖ bAc„ 2e7~T*T N:łsϥ9tPSNO^HvIHHHHHHH cTY={j2@-Ȕ7˪'E$]?ld' V ߆ >\)IуQ+|l]H)H0/F8ATTU.^&@Z "xI gJW\rrlʯksmk_TQO [)Yv%J+V*kPT퓾GSJǖW]?2bsbŊ2dm2͛RJRJP?~|w'EEErAi"vTiHHHHHHH P#r$<+RbٮVyd_ӼTH2C/kW[i4xcYoGI_F~҄}_6_R55SEv-]2qT~M2{/ AJLʟ?WWV{RNΖ d?H ye*8٭1lIHV}uWLY<,zMV դqTK[LY1e}+]vr`/R6nܨmV ?v-o[Y&]8, eXs [O2/N^#      (Tȳnl5[- O*|bqsDV]$G6[Y4a_ϥR#(̞F-_*L--*Z\]~;-eۉ\(]2ŋeӊqm?JNצs9*߼l۰\>nBIGJoqLyK\aDپ}曲u8t颷{^&Ow@)㫯e(|$ұcՕK,PZ#       P#*RFne 1GtM6,$k~k5"Y6-ٽksښSQo۰XVVD{thP_juYӤF6*Mem5NVN{<2a }QբfZ?V05ȶKdޗĮM:K>g럫fRVeʿ^qcZV9_YV1ڈw  Ǽ+ zmJ&]Uؠ1ޠT^T@.7-K&ɒq/?Q[r0ak_!vl5ǘ˱UYgcqb8زF,NgeغV6- 7]e [j?}FRj{6l,k&#jc[~W-ʂo򽇋P~\>C>M$דLoZߤВp-?aڟfNͺɶu ULTT3>E+v}}/˜;96?ԨF>wOf.ȱwҭ[7eӦg,˧~ v9Yfi >3?&NhnŎUVX)sʮ]?QV-;vL:U;C>VK>ø8cY'- 4@^XYr˖S       &@<|Lb:q-1IwU(Wt9i&̓] wt$pz/jN6ot=r5ڠ݁zŚ7Us)#콬 miUz[`L{vٹyV.M4zt:;V{|ٵmcLn :ɔ?w. V0zwϤ/K' ?33PX93iD;h}{Xݹa-'ZN.lS޸Fw{ qS{–oic5Ȕ䟐pB&Z]_wkjF~[jMN 2I?s)TGMfʸ/𻥯Bqm?LݶiulK,;FU~/GvZ~¶?y4R RD]2nRlʙKt2ûbdZ~_0toH?{~gZ~Ma_TM8.IGУq[&݆Hï]BI-桾 Jxǎ7cdž].GU .[DYTI&rH͚5}ۼy[Б !@C"Z8AK: dbN5};r<*&BZRԬTɲÎ-djs8Qj$zZ*W+.^5+g}*S߾1/~ N>z" v8G{TZ[}Jm|jC⺲\Ymy~ppoq[RU8aű Fk~cwjK-,Tmƕ3dR<۫skum?*רS1e-LlX6Y}tV<'ѡP0^߱yu+kmTe28nN ǵUou)A՟8h!(|@uJlRhi^_MݏW:c}ϤoljB|]\+Dme3)0/J&|ߤ#ZmuuF?yrJVŊs;XV2[wZZ'~#[0ϸE^5㣻d`zi]~@}8p\ 1i$me/(@9īg۴i#ta1bDl͛mٲEK|*|tYߎ;t|PHT"@yrBmذ?RoVy$U\G$@$@$@$@$@$@$PP#O򳜚zۂy٩V'sfOz@꫃Sߋי.ڤ7KoV5S+~Z>?L8jjqJ _ZYV˭_*~e±2?ljq_4UV0GMlAޢq?{yüb~ϚkSMt;>4LzKf}t6vt-~:ɔ*;}{Wj]zOAU}O5 3(JLq]lul{:QRV"so!ʺ@ƶ0UQO|lweb)wJѤJgHXu[~l3YY1J]Pyu[O_,Lϵm[K#oY;CnW6ퟷ}Ӟ<2MkDI~7}RտvZ~\wwϞvY,~G-Ե-[,_!-/p?xq¤ӿL˯v8/ו]´ٖ[2-0/J+y9w}[ƪbJL8MY6LGmoXRG TXo7eHR6CS> J2_ٷ#?wUCfk׮RP-RHZF% .U0L؆eҥ2zhs>Kz/AHvܩCCF- GG$@$@$@$@$@$@$@ @<) ;!]YKlNO NC'/ZV86+J!DMbwOz29Eᝲ|;q UzTb; Xb"[$މlV~;.gߤMZ9G5yt8J}iRej? 3`?لMS5iAMZwxӆ{a.S5W/~|hB‡6 qѦ䈝K>zTB+4yQ7J.G>򮹕pt;P23.\le߈i_)*KBu9]oX$$_I{d~տ\ˏKc>ҷ-V:lx-vѾGޠJ/_ uoן07{PEa6 ? 7uem-f{o7R;ei϶1ۧ}⢋I!w`/&ؿMo}mJҹ*|JNögywȾ:sL'Ȁ{y=v/^wGhꫯ֭[iݺVE-i       "@<'C3PK;Yul!w* 8+ej`rǦU)o{RJM񳾁0%GUntY!Sf[JȮG9g\7cf^l{?j0v'4Q%L8VZZg*+ 4S^]eٳ)\)|柙0۽s| iOzT ǽ(s?(TY=&뒟d7TT}.Lϵ aڏ򣶝ںa|ԩ XaeۦS[l߼JVN}?Bw-v< aˏkQ0Mvt-?.O*6[bmH:L˯^s}OWҵٖ[2-0/J+y9w} 2{Rh}^[e];.*v^mQ:sԖ|U>WC'…)- bne ö&>e_=- ,X@lrI'ŬrL0AƏI Era7n5jPp[oŬfS*|`[uԑ]JztT˗/ދ'|ԬYS *WtQ-x(Aϟ / &*|AW@_𲔯PY[Iiv0Wvn 7-mj;6 oW|$;ͫڷ@Uݖ}in{ƞjKgGcnRVD:R~Ps+ jja[*K| Xͺ8{[__m7Um;N#L#Q2i\ۏt&_?Q??A˿tḴ?Q+|).{?]Ig[~ )0/J."y3ϵ3s4Y(w"eAjj J@i,6:]d«˦er(eIShO?'=ɵ={~헉y饗d۶mڏGN/"D: Fj&lY6s?1‡7<( 2D#FĶ1իWom.H$@$@$@$@$@$@$@i P#-?вyf%ZEj9j tu {}Oc` &Jd61+.!oVa?̄'ٹ{ϝkQ?vٞ0H7aNᣨeoKj \WHKum?ҕs#LşnGlB2_eGkZ#Lu _W_(|x\ S~ma_]~Es_'9X/{sZAǖuzCwU^VZ&yNX^GmESԼ+;5{wqd;ח-`i&O,v[0>GP;cCeR17Kd+:z 5Gd;kq:a"@apJnxMpc57T-2Rf|P=F$̄+{wH,nY6-KҟOM۶ayot֠áŃͩ_XzRmIvm,_?Rb'/)|_llz5>Ş޸bx+B\VәI̺qm?~pαb4l]˯y+lRez͵?Q*|-.{Y?VlIG -?a_T]~C&│O&2g[x9dͭcS-u[Vʗ!{D8W:5u?w*cW<L>V*~bԩ2f_$u_^X6.7Henwg+_QMo Ji$݇ޣ//u=꾸GM( uU~#cXF6{x/o&&Lg o gn=w7ᄈu9Z:u+)3;6%hg.#ˆ-hHїf|tR/ٰw4lƫQZ飬T^lbO5̄~kF$}lHrMܫB)U )p "4i"GUdL6{K/B̙1#5;w /w?e˖| / &*|pPYhTVԘxĉJcS m=ҥ 8{e=+a^x2z½R^e \_1p2Y\'l]凨.:S߽EVNv}RO 7eլO߿,/O{,+d6ľR=UuDŜ/U`=R\uja3c͉ zmnߩڳgZF۹TD#~ g&Yo3)SGެ=5T{w9|jbŎn+1d3&O,:RJZQf͚,-U(mٲE|MٱcG, *hE(mHu2h С4zh6m}[  FqŤ-       (XTo&;E3Gɴw-R&覔.}e j41ThS"WV,٫6,"+g~-}TZ$PPZՔw*}k;XSǏm5&땥*5KQҴq&ƽkٲ*~넭HMY@1A˧mn*iv?a e&0ivٴruqSMTQzJ*UVY XԲt?e>V4X4Y7*EҼiRԴq4eEυ?t-Ҹ`UJJvs?Xp?3aP+}VL4PbKlMY{=A8t$@$@$@$@$@$@$@$Tr`OzNz+D虜hH06yj"3ѬmfݤI(ɼO^ <w8zq})|)Ǐ%\wB~W* 2VV=Qey@YИ,i΅ Ѥ?x|j?ev+?+dӲkyWy364GJ:-e8ݰl7Z"O7\ u:ߜ+{{YxSJ0ᦺZ)[Ioۏ<Կ./U3?,bY5cǛ˱c-\ϵHhCovL1dRٿKs@–.P06;Bh>~ e={vj+Y~KVg:]|ERPk['l‡k + uRh(޿.M) dkõ" ”?0]~'}kAp^(qc:94(vt9oʊ߀? f_mg8{K+`{ ҄<۶m۽^:6* CWj7SIWL8VZKzTSwvav2%ÖN'V|-]*a7i]ҵٖ?k`oҍKic{gÖG{T^OV(r5;A?zV1mxg,#Faa{!AO~knvt%l[ңG8 (z@k֬IlSN1cUREo QF… 9*| U lb,,X@o1 #\֭kyVC`dܹ$@$@$@$@$@$@$@$ P#G B+5ɸ[0$Še:5؈AǠBjR^+5hXWYI-/V{& Jʤ>jtl]@o,=a䏚?&kخ9lGZ5U寵*[eںI4F=!5UY6X,r߄YFJf}m;U|&7h#k4T[.zcds202SF=Q،5sSE8QOL˯_t-/m(GǮmk?;(ܼ:wȴ0#??jVܶA)y 0d]HH W 9CHHHHHHHHr>r(W e?0,'i#       2O 9Tȡ̠($@$@$@$@$@$P Pᣌg0G$@$@$@$@$@$@$P Pg1H$@$@$@$@$@$@$@$@$@$@$@$@$@$@$PPᣬ(C$@$@$@$@$@$@$@$@$@$@$@$@$@$@$P Pg1H$@$@$@$@$@$@$@$@$@$@$@$@$@$@$PGaÆ de-/               D o> @Gd2H$@$@$@$@$@$@$@$@$@$@$@$@$@$@$PPl'SC$@$@$@$@$@$@$@$@$@$@$@$@$@$@$PQ$ -T([Ԑ Q8w:Kz!5kVdH$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@ȄWHHHHHHHHHHHHHHHH P#‘ @"*|$2               iTp$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$H LxHH<=zȏ?XYh l߾رc -^߂n&HHHHHHHHH@1‡?^%HAOkF?d9餓R<[$[ GN;4qϞ=rGVPgs\uU2sLvJQx`u%>#uԑ+ʋ/(<@`IHHCD#7l ˗/ɓ']wy*~yFIHHkL.ejnՓQFɛoY{L #9r\{9c"GZ~:9ӥrʚҥK#pIF?1wƓ#yFOիNז-[ yX:;t` .щ?Rjդޘ8qy{CY+w,X SsͿTa=IΝ;e… {wR|Q!{l޼YM۾Yf%&I*X\p4j(OL fϞ=[^{5GsZ?n6_W~~?Wv ߥtMZuҰaØ8l,s$l]D*p(>EEɖ-[J bSP'Y񻼿BYb^¦?JEv;0o  /`nѵ[\k1ƿK4s3 ?i֬YL|ٜ Gm x?PO6am'ʇiɵ?ӵ嘨dĤ֐!C PKڽҹsgG--*(vq„*ԁvm'qaU%0I֍7 &'u/c?x_yz;h`rN:%Ńg}V|{ a0Jw_o~M;-Ko{9W܆ JSċ'_?Onfiժ/_>.PvwĽ׿5_ćx≔ܮR,܄J1|裏RJ l]t련+~.ɔ5lV~^dx67֭[eٲeZѫ,39Mki?WH+?, b%l/r}o+|vMXVig3~[(?)"k6hcG| FDŽT0G5QȐ/{ȴmb'(%-z z~K1A4Gib\:tTTI9Kilf9蠃($e;$bdO?'JX zH}ѨKFl꧋lZ\;=tڱ 60yW .?G`d% .DxDSl?!2JJL~ce NQgR0T=q}K:utr1I UT.D` Am>rM$A>\$& EEE&Z?HrݤIXXXbW{&(?~qEQsQUlv?[Yd%\ۏ(_Tϵ[B/bcǎH0.y8T&.a }8D.V X' CdK_QJp8@IDAT?WQՄݘtvgqFuf-w׿%A3>*|hlY|d>1ɌJa;3rJ=1|ܭQ DQR n_  cX}%X n`W^ @qA+(|1}Ѳ"Do>~(C{9r?z@vtPb!?AucwܡhzΜ9I]MzZ2w5\akM%k%#Ǎ 騣J)Jӟ/%7 #իu{gfޅ3Th;ز9GB"_!wE]v?0<e,D]XOp29wm?(Qտ(=I^T(MyW +l*RK\TL0va:*|8H"r3~ $Lu-]_&ZoF1bDAbpiàGZD+5%̀ VQ;/˾ȑ#4ReX E=ܣ5ߗ*R2&~IEX_(|aG[lѓ GKߓO>S"}뭷믏Ā/;b V2YǀsfK \Ba0ל)a1#0ώ??,\v/?L Xm.ao *%UEU#a_k-a.;$sͿt᧻_RJG_٨/X*=m4|P>(I\?D,_B$6V6lP1cVטLǴYi_9V;4a +.\(+vv^` qڴi,]Tk%[eʉUhyiis/hď(VtMX50]\Ρ݇[l;1؆I0 aE^0MVgZJ`^~ $lŊX͆  .X|rw|ra2aV\p4o\(`2u vx7h@{UFv#,(֠Yn[lo=fZ~ߥEߛL~d:Vv8C2{6[ a˯<3*a:6+o?#=qmP'XnɈhkJG=(]Y%dE0aPni|~ǰڿ0|;w: 36~iW^y>Aq2E(pOaL^9|0h%K)|ʏ6/ɓ'.Qmga?8(~ɠ>R S~XHa+}c#$w~柉7׎(co߾IsM?hÔo^2 Rv7wչK1h/6~69w~P%˲_W~~xvA7&;fZma?5/v?+g6㏢ߏK[|06EQ~LaQo䰏lq~ ig)co/*ckķb﫯IyF~õLiXd>>E>@hļ0o=Xa+`>&hzAD]h1}UX9] PA %6soܳjm8  $L]^IC -m 3߰кukwPEqap} (>9KfE_{ ʰy|wRZ57J ܃2 p~JH?= c۠>[s`mw6ߪ ۏe /LDž-P SBAkիW[ڀE2gXu0x_w?;\:72ayj/^?~ֿba?|ZWo?-XSh߹(pXDwuʼn{,럝lϜֿL72:fR%0Jfd){ϵ/1ߵv?8ffQrԇL>(?;l5M5_H[?e}xYZ睹-0 PXkuߕ]lo9Ϥ?8)QkÊrrp`|4$8{PX@[c7|س&lt wV۶mu?p~w#UI}Ϯkx-Fy ha `uOMw}SkAK.1a)OE̶$PĂ 8k`-7=̀y: Q:'m9˴-F¥(E vڵ&$غ@NaW^,4iD ~DBnA'qE =]_^K(l_EaСCE'H5U摍ו_l?\_]3+jӦ c]B0]s-K;HI toW]9lFlo1]ͥGyaԌwc>VmhQkg/Q$گ|w&_,11y"kE=ϴE,oos7>r%'\˯տkU VSoB#3޽{XRQL9CXT>X~U.LP*:a7VLHѦY2INlOpl"|X@94H(Ƽ?rF#dAQBWa~e'\2RAO>LMv\VZGRY { zB(CI 3afnϒmbd*v(g=L7&JN}3w\͌m̷_2%&oAcǵ`m]s oP;.W苘 \X ;*\?2[+l7GSJa5dZ2w SUXik_yU>H/!]U~WFNѴC~9#Hͥ-䍼ޱ(ʏK59+YX%u=G~SsCކra+*`aa\ 1qMGT/]a_K{.\G.䂏 楍:;AbPs]xгmv/[oվٖ.a#)%I-4nôwumR$_(Ɖ'(|` yU͘Nb4N6aua&MrƭEPzW8&PpLy&GMߙo\Gli)Qn.@9,-2"~bXH}(`"^%. to PT4maVW& }7%GLTxǡb>L Mofdy>Q4,yb5Q65fNAANa߮ퟑ% ~lcW)|ث [, lӧ.9#CCa?X"0Gl`sL8r-ISB7dq-hG7 _saJа8(g7-3*&>leTjU{PZte*\(~VFY:ws~akASL\8m}PC0'ځFi?,Öh*˥ 4{l?Y3&^\A_]{tPFǂ/ PL 1[o\èu ]w,L;T'9\.ߏV3m-dPeժUvפcAbm\ǥE&-8Jr)7,Y6+yzU(ٰb14`9$\'M )zeϱZ _|]_?C7V4`*rb;X@'-y|׋/8$>eCǎuxAaw co߄QGN&& Skuߕ?aoIE+0ƈ[8סC 7^L>0)u. ״aߟ][m L{11 3DJ ha,91 (ra>R{#O}L}F0ͤNϢ_f XuJL2g9{eRmo`aH%>uÖBbC;~dҕdaكh9ߖ?f@V*6DYLy/}frQP_* cJ¡ ,=RZcyN?;Z(Avieц Gi9,5;#v/c?f/l?\_]{EQЁd9W\s1K3oL/0vViv#h͵m0R(ʏK5E+(eOόs|C#wLuY#Aa҇.GE,o9 9({~*ъe+8K=¡xhK$,+/h7&zM$K͗X@.ƈ5I,XU@ 齃qYk֬gy٫;)k;ZňB*|4Ceq|/ٳg+evŶ8P8s[N 8;U7ʄ+IP+7{.†cL`^M~_./ͱXq?mcaƙ}}1l:aQ8(G]y:G϶Lj[IҿTH:|WMXyƶqt/;LQ&V l 쭷ޒo1b#;(:~wX^>?9i[~ (G)?/_ fQʯ?JӿVcsxĉ& XA؞& -AGF~h{?A> x= :,ek׮JjE?E뮓|;S~az>~_6TT.?z9A:0 K%Pb%dҼ pPx۷f܃HR"|~&q= |1^օ*|\kOXuу5^եٔXC=`ʒn&s׶NqM"lu]<=J^[ZvzZFKo׿_[~I+I?2n8ߖ;>`Jy)ӖӸO+3?F~403<}ͦw}7~Rs#~ɡq,@Zo#YNq?g1 f &0>]ׯ_0Xm=V(9>tKty5AB(q+|[LP`'&9aGpcE̙3Afϛ7O5/ Aڱ OcK,Q[~8>į >XN[~y8a=d~7D{0P֡~G(/0~' ̂a.i~am7ֿ?jM0N<2эQ0췪`plرUp~Zxf?)0~%݌34‡)~: e]zOTXu5$\l˯m:QҦOT =Iˏ4E-kG)q7 {aAr[]4;?|V(|U~?u_)~ie>L[3/rs~Jt aNƯYF1-Rm4(5sLòCBr)/vmrW+'(T%r`Hüf~ $38S~(T-ʊq/BMXmQ~ab{ 0={3t\qm709E+kWGipŠ~6GeqTzK嗴BolVmo϶O4I:(,vX,EdO?m_켎3qm_LSQoqMAM?8GeU8Wt'Q?7>(÷W>l{%(ʓF~7hg /cƿtN4!˳>+WWP‡^Z>^Kh 1xe A-w³^0 'e˖ &9٩S'5?lذ:_p &O  X5jyKǩ1n89cTaqN9ᎉ_Gk ֭ѣG׹ׄ@IJp)Uζo_ן8>((?[u~D-?~8´?OΝ_Vƍ3J/&ya]rYeN^:F6mߟm۟B(|tM̙#_~;r-愝[ь@¢ \ƌmIan_۶iZ?#gҥK3,t c8(ޠlS-|x&"rح@8G FW K\G~W.q/pLOȑ#%h_z3[fpg?3K[4t?[\wu^by^?+^W\7}<`q7sy7o.Ch[Pg4ó?3,J~V{~>y֭KAW/g[XHܽ{꟠TxjD ҙζ۶?fއ?@A_OرC)yBR;h ox9X%&;zS[ϋy-q?k_3ïm뮻24qw8/슝q||Doyvj?bo?40ߏڏ5|ζu'loʹ,Kَ4c~Ve`ֳŌ+nöiq6/~L{-ST c"# Xg:X o 8cuLI/gn9&8yO kV0թz/r/81CIv*C =z(P{z:'k0s7p2ޠAX65V{bB #A 5֐O?UZD4-2(|G?1E.1籍a &yg2D<`ܡ4I꣏>Z){輹eY暰g0 WQLg[~mO ke(92XކAܷvTUUQ3hs9w-i߶[b Dqǫ,=>cEG(,c g8@)V8ƻτxhamlRM%3WO PQn?&Q~ޅ3"Nm׮z^UߐӦMӗ/&h!vfQmƒ\e XA峔GU}ϬZy|&qenyzꩌe> +^D6rq?Sam=mat߶?ݻJ,⠽]|(mW|"phNu_Y?LIԿ|l 嗄ֿmGG_ 0)úb^?M?f6/3۶f4OG/(|ؖ?mA/UnW?;]~~xtpo"q|B QalsG6]Gi?T?ڌRTHys=j Na & ^ $h0)^Z+0m::a7:FCz LҺ *aa:wn92 ^-$<~X&BL+^=2e=Af5:L浨acӪX궺Loc TFn xֱ/'ٔ8Oke폎Hgu&:zU~>snڟќ/,kS~u|QߟqiTEzEowNA&=mgz\.JԠ {ѢE}Ž>GZU6:,P6Ef:l/I=()>-\P)ah\^Ƣʏx'uu:4h*`9(\`6_}Ue PuOn@X6L)8PL|)㌚~?6J9 N{)0N=T*mCOs@7 v 뒨^r1.F;II?l럭-G6z\㌈%~e)?dOKC$/j!ڙi*gl:o]6mqď0x!3*8گR3;e1f.T6KGg mvLOSDsМpRa/&(8@cǎ>O|ԧWwx&'s)]@3Tu0 1R?U֭[Fi 3s RW0C󎯒;,!gmh{G'i]AaJZDzeԋ+z2K̏\[&`d˖-0Q\⁙,s`^Xv'P؀5TJ6 K~0Q2˅g߸߬?ZS._MZʯNoGojbVN2F՝ o/|ϸGmR'Պ"Z0醸{}ƍ$lCW|,Qa! Pք8WIrD`,L8g*mwJD(gV0*?;3JH>_?b{ '?|Q* M?N`m4nCsrz뭪~LvgrQWm'67V6ĶtxiO[v[ayi]V0l ʔwvPtEPVTx%QKb\7*?-wmoic˨/)K&h?26=-Yȿ鏣4_]'~&e_mQQǯxFö!nkB:ʭ*<:~W7|c!,[TYaL 1뒹6GZM1\SjQj9jZT3&׵y0i^&0a ɼ-L621+5x[!FkxQ~ ⃉6WlݲtIʌ̷ńۿyFa叻Cl̘1(-bb7 #l Ą>4{)SK~m?Q`}m ) !G'p ;Ⱦ_jwB\?FaWw~l[|h0XS;-pXD-T.g\^RU"Q~,BDrpѪ/ ACPķ'ޟP P*aYy[6鷭vU}2ڶmw_Ҙ+9d\b2`"UqX̀}HGB;:]Vk.oh;ViV\̷U4^ӑ PpƖ8I8 bZ\I$g$$8I LR" @0lqS$#ǿs \Rs_2PZ2Uzj %I[&`/Ix:q,21be>+|ؾ?mΉkvZoD2^8 r6~8Xa7uT[K,E"&- &#R HHHHHHHHHHHHHHH(޽{KvTfϞ-;w J>I$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@@(|#L X‡>z&               G3F               "@+|L$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@'@3g$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$`Ed>j6lJ?,K,J8= @G&              ([T(۬gIHHHHHHHHHHHHHHJ>J5(7 @Gf=N$@$@$@$@$@$@$@$@$@$@$@$@$@$@$PQ9GIHHHHHHHHHHHHHHʖ>J4+?Bm%7Om~C`M?c#P=&        "@/*%p).\$)돈l^mc/+vWTJQIEU_m(v-cLzz-u HHHHHHHHʞ>J4hHF^TV-HܔV]Ivrhn߲ᤢ\/d Y\׶¶azK*i{Jf.#e@ٷ{l]9KW(o}dm}|ٶzܿ;6mH˖-euVYnڵ`UUU8EpYjUxHHHHHHHF EC_DսGg pҷe?k~/-*=OcQחx:7 }i}pΟC6]q8{(=Z>w ?-iS',[w};7Ȓ(W>.UȾ]d 'CI9>}3Y4OBfnf")ӃB_j U߿A}U~m˟ lK] io@IgqRٶVc)Od6iq;Niy@iذQֳ3ӟȺnW߳6ݾb^VX(5'չ 2rHҥK1{l?|u^8cO>yٹs        bGHܙ?Q8ɆE uv!=IDAT0Q5hrXp}g?}I$7jAQOƍe̘1ʪPhҤTTTdYfe:G\$ @ P-@랓F+db.t‡%hAx{am4:f=3fڜ?Gg''i/K24w&|*dKTn*|%B_j# AJLZW~m˟|lK]|髏aick:DcV>7Eǥ«:EUuTYoFٱ~h۱3~}1H]YS{ʶ5seǯ{Z'ߪ+T0k$./w* l[%N5rl+N _?ߤykzVIrp}=U?f * 6n֪Fɾ/8+?ʤ&M+eϷ~*;k7, 8o ˿MC3]*[9K6~:x@jx6wy54ԏ[is~ߩTYSwg-{8r^,-t]!z`l]=WVS隷ݮ,90t=L)/g~wnX~*sy߮MSWH'I#+vo>Zew~p߸y0s_پ4w4h=k6lߎZ@arL6.ӟ𼇋i(?w.aHKmdSwG}ҩʯ/4'JS3RiY3@lYᴿk,iب_pZSj?/~'ٗ5נ71Fo;>m_¿MÿRLK?vV1zx2B}Ι,?1>Z>|floÝWa~W~gïzFk]v;}]̱7LλnN{81X9_U8/ 6L 4iojذ\y啎l2y7W\Q}dy3yҥKus͚5Xi]< 1~x}9/rJuuTUU`d`8CN$@$@$@$@$@$@$@$A Pz C/{T33-_Qm>4h$νG s?='mIާުXLMm{,Ϲ[y\yZgço?t_vw?V:Ez©w~yop,]{OV\0ػ1 JINݹu5K}oQxs*|4o\.V;`]zō+W믿yE2fu_{5Yzu>F%={Tז.]*[6cq!yo=N:v('pTVVzٹs[Б @\"kR3|'D0v¬ӰˤI7aV9v8UTIA vmw?MWQ^^1N6o.o^ZwUvP/ySL [\ Һp5=yam+ vҤYKޝU;V9+9V>>~N3:Q&lmGqiҼClϜ0ļ?363Ǥ;V"Wc:Iol ]Ȉ33&\+;-1NtM:>x2Dzcy!VE4k#l_QzB̂`LԶMyr ~^~[RmN>$ʏmaߝ;VǦ x|e.W(a +WM˥*_fNVIF~%_[f'-Q|akRJ]~Gcm7el^+,.t,so;SAaƱ\mm$; ;6~"=sy[)m$;o,832}s^%?' PQV16oެD̥8m)[@۫M6MVXZü dM|pCA;PHs(}r)Ҿ}8HHHHHHHH  @IDAT|UEOhI @B.͆"((ke-kw]ׂk "Hキ@ - $! w}M wz;}ys۟RƎK]vN8lR vOmPe[кoLéQ>yk8}Zomڽ}<]DUɓċ8!FC,ent=\#I+ԩBڭ/K.6 +VOɸѺJހzB11D>J7߫2q.lZHwIοGFm%ڷs=IIq2ntht վS=KIɪڟNgV+YFqp5 ^IAͳ_}+Z#:.׾CUku.7zկ9z'6EqHxfz#T/byǧhUϑļ#tNrlڤ-B'ʸM^Sҭ᎟k:t*hEr;ե)(eУ2.c\1?nkmϾy*VIʹEBL]/3۾;45;ڊj|?~yBèE<8yNKe/ûnk"U3}'PwnJ:PMo1\Jlr6fѱdj ;S+_1>i* w(ϱ-ӨRm-?X'b| J}(kZgլ5I~/޹=ڳ#֭uAV_PvCztUWɨ5kҥK\YhϞ=2sX6lU\Y|;I=5k֌ڵk'n޼ϟo-&{Em} "ٳgy @ 4y>s:~W&M/oNW:.@@@@@@@@)|8EW'j[6v_)s˜. %8G*v3'@v/_EOcdq󽫧іY{lw-L;]ǼG I"V}W^p;W Qz)e$*~:>yTSv 6BŸVMߪC^މY*Ix +/ ~LxiK\SW?$ޚ^-ޞŘUn=վSևSW qNQXrsH1 :/ł}J>^Nag9?` -xkOUt.W!SW|F[y'OB6oWFqϨ#~|./~P!HG'㧏+t?JPN} c:q1 5.dqÉoeS_S`uȉ蟛K:Pσ͟v*VIg0,(/7YCZsE^ y'5ZOڵqv9 UGx*%PP1# \̕йsg:묳ªO>\YmSdsJC ÇwҸB]ɏ?Hii^Tz#~ 诉:>/?)`ϯp >?ʯDN]~E?7'TU>`Cms|ySGm(cb+bJ)_x+p`+>ꃽ'vC@rE떛 u-ν[>~=A7>֪U˳=mp#35~NdqÉo%^_S_ݖ^_!'wn/,B=W矩*]Q7ɤ=.e:\"lғN,o[Sf#[^8t.7H,Bm<$w.|vO!Ԍ!/Vdn<ۼ|,]VP >+aQ3fPzz Xzu>h֯_O-^8p       P`Q4Ua'|dѣikiŔ[CH 7N*aJ/X[M^k=7Z_%S&m:Y2+[߻f-'=f`%"U)_1Uݥc!mwǧ=i' SĮ]˦QښokGiѻ#|$OXHr;b6GzMϹwFF|gc6n/Fg]=:?ʗvu᜛>ҟpdVy6߭TDŽ:󨷤=oc#ahdO^gz2?k㦈79q!dE[XNP9?tr$Z7Ex0:.wXv+<(pDT_%\7_;YJgǎ㪉i^VMo?&ND~'eg5W:TUy>:?ʗvu̟?SZókX([d_/zj/z}/5='ڹ9m=nⅷpVBd8\2`c3}=DVRF)[^j-_G 2tPɓ'ᅲ>ڴi#nڴ~wrlrEm۶Ν'99/mA:uЉ'?֓9 6L~LKK'"@@@@@@@@ |8VBebWo*j^x\!etȜw'Op"Yi//qU~ Ti/q}| mA{='cb`k*?j¿=^F-;\/ln#ynQJ␺K:El^q-p#z!z!5 !<|SyOlS\-|IW'&k6?: {FV.o[FJsVO5Cv̸;jQӭnk=S.~(+],{1|x:kr'+[,+ruĂb[ai÷EBi:QtFzaA.Y)~B;]F5+_K)miGu *WE}(et=lޏ}Fj^:O! uzrWR h>ئ'[SY#أ?goT]F:zm˨"ohh">E"WXa,wb\5:Oye]/ 奡 U];ȲY7oRb{MsEm궹ȫNu!$8W^| S fxsH?>hM/.Z:sPFqG vd\ƟWytLx˩Ԓu-Q|oTucwR0:%H>^cNn3M珢_b~Gv,J ITy_9~<>{Ier+*Qa੊\0J[WZ΁b+N]DBL܇p?our{/7-5_qVDz3}W?=>:?7ʗv''5y꿒:D;$L eԠSVbRv&*'yݺ?%8S5=-vܢ7 }>Ph'3Gi0*aV~y"%F|9I[92oq}4lؐòelRnٲ%WFъ+=a.lѢE OS^ȑ#hPiԨ]xرcrnC={OիWmRzEl[x\n]4hqOƍa {a#f[~߯7spLA}sx&AF] ak=$2}VmЁ:]0PW\N]I?4=s Î!zE.7]uC~S[>e7YmzrCxЃ @ 6F=cvk6b?qTpV-οx_nN~M74oQIHk$)h:c!(o?>gTi[ü"-L5?-_8M=nۜWԥѤ"Ο`G6*mEJLllx %ˮs?=o.yw{Be?N >LשIY{50a ܆r*8?UNʗvUΟ\˚I7]OA=͆ XӘ8ѽq?- v "K:NqW|\:d͕۽dffz%ʅCؠbƌ_~;vМ9/*T 2Rv HfΜ) Q*?}ѵkWر]' I@@@@@@@"H\{Q1n|}Xl$4v 5~?eH[BTg6j4Nto*ӓ;J[-{D[~Ql$=3H}.X5-uKc)N>8ѽVqa%Vn3Un%j0J:[z/&nȞe/[֣8?`7!Jf1 io}Sǻakw%:oAo}Zo78nf!DyfQJ[f.Qo/QlQx`hܿnܷ:ȪI?ɽZGᅯv: -?xR2[%C?.Z#`jRѓ(^xṋߖg䣗sq225TVhK׉=z:?vH*tN˗vUh2*SW;z*U)4ӯq^߻Z[d2U{};k5o릩T1_>ؼ h9w1}_xfԊG_mO:u$=k(Ѓ/{Al,~tL^ )H39P}U Mt.*9Nw?/l"lr;-/q k"8$m RgA-%iEPѽ>{k*'5JUj-]}OP?'o(2bTVc}Q?zTU} a9a?pGoÝýv[KZ~kK*un믩/dT{X0*5?{P![% uۊ-dD~<ꕴL/&xÇS4/{ |>}z-P؃V@@@@@@@@88Pg> l q|x?ӅZQE?TRœ[IΗ۹-N;Ra;Wʊk%L(.-0'P_\_qE        g|D('&S/Kɲlu_?R"dM|ۍ*V絽Qƶynڃ*ٵcY6ps\GJK2nou ]KnnD]        g:|#@&b?Q޻6|(memeHoYfDH        Pz$@_ v{d/ݷlQE~lyxшNF%^?*,"e.z   ֑\        HQ4*3/(      g2__w&w}3 >haEQ@@@@@@ '3|=3 >!FA@@@@@@@@@@@@@@408F8 bt@@@@@@@@@@@@@@@L#Pj >ZjEkזW\I999gX?                RcRo @@@@@@@@@@@@@@@|AFA@@@@@@@@@@@@@@,08(`Q]8O@@@@@@@@@@@@@@@ (5cǎ]!8q"mٲ                `1                `@@@@@@@@@@@@@@@@ >| @@@@@@@@@@@@@@@ >zx /Ā@@:uUVS[lIիWKF!3)0(SÍ΂;FA'+ۻw/]qTZ@@KDn誫">zW}ҬNk%}=tn6.l43ydoKk%K.:uvy~=^r1`e]F+V[Cq`bi~~ lV5?'-%%}FIrd{`?9Pp2$16[lI!=q={6}%эh x ZjIyϟOrKT!@@@@@@@@@@@`QWNK. N"mp\27|*T@YYYCzB_/"|X+rsMA鯩w5\#;pM4>c/p/#>'[pZۘT=z499cIS~~>&^ @@@@@@@@@@@ `1z ,E"G:瞣!CovXS^~e e|-9\pwNTL;?oҭ[7:u=4uTj>Sj׮0a}w^_/s7oܹH#'kɂy=n J~Ǐ'M6Ie^$)?@= kھ)69 cMύ>rτuʹ9F U naÆɹe-Q Nm۶^u*G[+6mpx=uftW           `## fC_|!T<3pBҸ粺̟?DK,!6 F%p>68p`"NVtm-[vܚ%؂_Wς#GoUVyD7_YqzFZ~@h'*T* R&Mdx:mV( m@7lc#Gm|aye1tԉ&O,'{lsSY           M`M‹osEi^dRPBOn;'xmի|ẇ6XPvv[7葇OԂ1J 6 aP%>>(--J.#N.+~6`lDDjjMw}޽{(c6.k֡ƕ_~̊DžWb D/ >8nǎoӳ<.coW\Y{Kޢ)pozHO:7߫"qde>](..N ֭[' #Ђ\@?Wo=iiP*G'fyP/Rq7LFP̔^k_j7Ο@׳gxPeoϝ<1B<|D~ l%`;wiV ^{BbŊ2îR6*` >eW2tk} x[6x >4^k~t\oب/M{1il nޮ@y&acG}sϥGyD~pvJ}R^Q}>Wl[Xb*o2ex랙3g-g.[p`O>Sd7馛= &[t۫v.f̘!_os/Lϛ7O%cSJ+θxbeWaqa'o巎Gwn Q2 %h[AM>]z= .'g}&c6l4ƞυ|Ot4?%?+%ϱO>J*G9ڵkdN@Giy{JNNuo}A; >X᫯ŧW^yEnWÑ8+t\_溹 = y,fEզM{Eo6 ]wlǖ聍@/v+P6иeU~e[SOHzaCF#@GɱSjn{fbpwΡ8 >5/:rE%p#kbʕ|8lpaŊ4fO >K<1Cm iD~kuNUC͛7V~/ܹSxԨQ 6M';2wMʯ;?7u9=oӦt_JdUldžg퓿 <5j${N>W(v)9aHÛ@@@@@@@@@@@ #FF` ^0l0l$ ^@[/X7ovm<Ї NUdTi >T>ʷ8M^u}h0P2_oˉ蟛8K讻ƍ.\H7xߪ|p twH/E~+>Pv?UF72e =*L[jҤ <|GA@@@@@@@@@ >tdǁ9Xozp-a`ё;F={:%77w.m|̚5֭+\<# ^w>o|r:ko{ظq;o8lٲEnUHpI` 'wnr뜽=S:F|YŎ;ڳgRnu6pJӸq/" eTZYΟ9o6lpUeإKKO8ς}HG$'|B۷9 bl 3cԴiS*,,!CxP{9v jyrժU| &Lc3fIwF;w$^Ά u}˅rݻ 8 CE~$y~licM{z!ꫯ=L֯/js;UU͎ƙLOG ڨQ#LJ3ovX^j"@@@@@@@@@@@ a]wEv,rIZb tJzCmwqG' B~KF V:N:2A}9s&s=z2)| zץfo 'O:Kfϑt׮]e:opM7s_R֭奝|SN-[~^z%U7ݻ-]x`ϗ2k֬D~kiĈ2ڎ5?_ر&Mچ3,=Q[+ކ?\v]s;&Up,Mˇ;C|qb׃n0\{zynpKmdga::(~X'Yty%ϙjgʇ#@G?:ud[f A >Q ʲXʒow뉼ÿo=sG}^d.((oݺUnu' ƍOYF#~[ ]Hɲ{km>W9g/ \qԹsgj޼tM+Z^p!g _m _33[n!yVm*x6i֬\=udNjiz෩ٸ3ko۶mrZs:/(jL >z!B0z=Z1้֊*i,x,l_ ٫Lq^={4_>kN{yGhڴi^Z0-zR||,|TM70<&s磎2@IDAT>7n8ihs3qD۾py5y(?l@^18w%{}~fq:߇*|Ο|-[F*U ]^o?cIct6fn=.s]oG}YOBϋl$^*]z.2On߈g@b۷o,ҐEyrfԽDXĉrwJV ;/^\8ߦ*u=[t_?ӟg<93{M+e96b6уѾ u3 ܆r*8?U&?:ts 콂u7Pp2?s=o6mx03&Z/U&mS[NO#Hǂp`K,]n | g[L >5b5{ LիloQ]cc^0R .Є߮Z~}viۥ}6V>|85ic3.,~ 燼Ґ!CáC~ ^ ~a5j79/zܹSz }VF>E7a/ /Brab0Na~\W=YXbA2<]s'PeuKU[h!/wUTj)a2nSsM6C^|/*Ý .6{a!6lrŃK ttK<r,3 7KΠAg*';;[ŋx8D >"<Ng5>@wZWi( [:onhٲ%q=Eϴ45kVE;v(˱gp\oHq60`l"7*i2mJo/^ 57MdFy?6XcOVo4ngW~wyr իW;LO;c㼡Cza,[Cd,JuzO @ #cÞ8xbŊ,{--@-OHo(nՍz@ @p٬tWSllo>aܙDd̘1UTسg=a*8|[Wj6m*իWʕ+{f#G\L  /h1´J#-1e!6Fݻw>'3 o6aboHgϖۧIC_@@@@@@@@@@@L"(͕+WRmܵk|#6XfM5X-iF|`h @@@@@@@@@@@|8VE+/̤͛7?_-(!(               G@@@@@@@@@@@@@@@ >d JB>0                *RcѪU+]ʕ+)'''>"QJE00                 0u                `aEA@@@@@@@@@@@@@@@ ` h@@@@@@@@@@@@@@@ cR׮]eW'NH[l16@%;v@@@@@@@@@@@@@@@Gxt@@@@@@@@@@@@@@@G;H               PF ࣌<                Pz $(`QJ>N+0_R 'Sl츁d^ǯTP>v-JR{aRmI{X >sDȎ%Gie}Jb(E[5lM2!ʗa%e5^ JvaHQ%y0(*SzVIݑt"WO'wJ dqW_VCu9`ЇP/8RBUO7ӑK)?;o[ Փ#;~[պROqwE3j ;Slt 26vGe}"փz)q?~3N:zeũ?z;#ݾ?΄_<4$p4HMϴ|(lJ@yJB@HL9&69*TQԝSvM0',gnA+DR0ҁ $VRn?dѺrbc:~8?'VsЧeߦ Ƀ{/#F$yq.DuNA+ѡ]KhW)'SCiLS_D7ݎf鯩mc$k;ZoQ괢փZͽ/)"0(EŢVRzKyEoS }z$Dic[i*^Wנpzj6(n^ϐhZ/ۮs%MܑZ0 UҚϿ"͈fPt7ZgZ>`+,־J-WJ^a\xPBt:u@7P >n%T~L9j>JHj%2õ5o        PZࣴi9uV}J]moqd*h&js) E<>~Ѥ?ހZ0 UҚ/[j0FNTL̝ȯwrMw$*hͧd(cӏOSf!|WAPݔeԕc<.P\[5D|DۈGJt}|#ed[Be |Z*& )X&eB˦P~vm*bDZP\t"eoPYIlz)԰5PaqJvxC1Ŕ+OGS)sڷsՔT~kW*נ]*ID's8s10Ce x4)Pl ã V}EGv/yJ I{d/'^$kK]+/36Lgpl֗L-SHGmjyl+}t1 mu:~ ;~{jG5jڋ{਼v/mU 95Pl$:YGytdڻ[ٓ #{JU ?\=ǜ-{k>Q%\C5[K5MU봥('c+ɿ߫\ UoKuk9ݼ'iC19l=.e+QC:[[en] ޵MhCqgFSWQf}btBt'T? j :PbM%T|y:ڳd25:jPl/YLG} ^ъkegU ן_iUp˛>+ަIwZwks_+|%j~T#z8Ss O;G{(;\G g]!ڿVq_ѾO'       QHQ8(DE.'dk7J11)eӔ$cG.ĭ.O"]/7~wŦZ-ϣ>%Z#֓m5Z˚o4\6ԲUCцO҉*&-m)Pĝ5/xf)Sv.ָuRj):jyo\0?,ҁ-sa9S1:'#{&DUMk;cۼ1CxxEE_C?sTt[W!J4p5?XĦE(.]wZg1?N_ßGκuDbp*UoU|Z0YwѽdZX-I0ܺIfgx6F2>~h7qb֕{X?oïcQE{sz8k`-?% r_Zy:&Z! N )B8gzc-^8V\> E B }am \)ކ%Sxf c: yTLedGAK|]Rĭs (~c 诉z|^_S?`VpW/D, G~jN9M?}wk^)]^}9-|gDPz|࣑;du쥎=[ /@$ #h;FM_Gdѻ#x_0]0xKظP_WS]$SdܚYo)k ŖsEN#_ۼ?}Zߺ`“Yܤ߭/v/}⩟OLL\˿~Q_xLs:e[tpW|z+e['}?{VQ.3hF5Жϩf=GSTt$\m.~$K[`ڠ#ubK8nga:~J0V:&mp: *63">%bZge#?󇮿{mkS1?[0Z,R @t.j~Hla)+m<WM䷶__SdV WN\Wp7''ɹe~c˞7ώxQ/\aA4XKĆСZ?@4G4B2n3R?.svkR f :;o/qWVB.vҽr0 wP[O9lJ_Wy}6CxX'Jɞ ũz[|.liG={L[L=BR.~|Jx^%MNAlI[ræY/P^֋p5?:|Ux"C,~MRA2h#c\1?nkm#eDL2qc2(riEF: n'ypEFvPہ>2Kk"zY|>M7[u(\;?7s]IԿߟ|o7?9>NMϔUf68Jr[!];5| c**i+E"D|DـRe-se f>#< #J| >@bW#=}"߻zmZv O^~=Ec{D<"-s^ E]n;R.~L6p5?z$Gl;u>Z>Xo#Hɠg#mH|8?TL@'r-#\5ʴ,>L?HoաpW/D,u+'/n+ONs3D`e\q?oZB,U՜vJxSn ִLw~H#D |D@RBS*W+ei f쪱G)/;r=c/5J׏-H2)'cTtCy' $+/ ~Lxi+JY.uփz]N[oO[o󆢷wj)}pJ!) Bn+F*/sSt¿(.egne^W'u3PZ0?v5̧ :Pސ+>'n7c|d=Ɠ'\9*_hK4'-L{ `NOp#(B9:u>vk'W$ >g0Ը`m'k"MY|>M_!'wnw"'燓''ɹw"s2n\O]}OnSmێLV>A@@@@@@@  #G"S:B6D``7~-5:*Ui良uD-v,Z4_ zN~pV;/ؕ[ur+4sCTNG` f,'wv `9`m'k"MY|>M{~caOT~M'wnw"'燓_0 6Iwrn:~=a%,noW?@@@@@@@@ #F! ]jhDž[ - ~\"UKbwp(<+drzBx>:iW qhh[USUy>:dNoϝw__ppqJn[p8s>ynqNs2Rab 9Ed.Xe,qt]7Y:M?<(B9:jߩ?[0#35~NdqÉo%^_S_|8`*CNW/D,D~5p o7?9>NMϔq"Uj7`\n@@@@@@@ >mD,Tk؉:_~NFM(/'Nnv2qf U)qOmgd,})>~]!ȅmC_wʿp Z]:1^M1F}G~,ÓA;q)}iTa>UR]a(m7*=NGPuʿ5~PA^}OMϹwF|gZ~Yt3o$vE\n4| gEr,Cc;uST|~~9?L?;vƙS5ʺ,>L=-1pT~CNWsS|~8y/ ?9>NMϔ Jb+á:6>U^028FQ>b-/zk˜iBӕF2ǭ>LzN#io)WA,gQa` P˞ѩ?+𐝹}8+- [SN++UucOFq)sz)'nNv#i8#ҿ' J=ScydԦ oSyOI|Y b| &T\K|ZQhPAnꯉOk_?NH˯tȩ|tnw*'wKnr}y8Ϗg>(wG;88zHbK"(6iƖk&䍉Ƙb, (6D,*Qz+̜Osχ{2;݇gόwk_Kdy[jӿe=˪RÊ&tAݏ    @* QVZueȏޕZ7٭qM]E dqa, Yﭾl+3ϒaYEՉK7奧܅))or[lv|/W >zy-Id/a:e^I wwfl:.}F>f*--+>[FԯGJFa$RsA6zY6x1p<=4==g͔ k3},wY`wU1rϚaXVGtW'^t:6P-yS+uȬWؔ߶c{p&H a=ЉW=+vy[soy3d KZVE %]R8ՠuհ ?9oV=g7j-m\/;u    U=7XYh(o_.pjȜI}eOu{sL}W^l _v|?l{ ɶ֏Z>^ڲ~Yi\r.74rTO?Z>F~Ksʾ>TSvQ㭻.g)I;wH7׉x|׸--ۖYRzHw9nk0+ebzͱBl[oۤݠkB7ߥzЉÇ~UϨA"_[UIe882sHgq5!lΟk{rU )ftCĺr/ ۔Ik=I''X:hǿYé:ذ%+Rʻ۶0ZyG`Qm>l]3߹~*"Cx>?_ö4Ksկ^#^{Ϳs1ۜ?'J!7ftyԞ!%;Ř a*==N sNN׍M_   U^NM+5Igv@o: +ejܟd,}_ NٶmD8~iKA5e-w`k[']/ LۇJ/oYxJVtOmOkyU;-hH^ 9p_D=.|G 5*ڲ%ᔛοx-~22np*/C9  Ԑ-_%tLg [pH&} #յדnvnSn{_|rߪM 5BY,~_L~yk^D W H~?׏.[sdH~Ӂ}[Խ_}Ły?H,L{-MmS]΂ʮ?q?w{uҨU@L9~P\B^߻k ~zkqr=s|!]^Zݞ~=Hο_ש^ο5yZ~?ꟗߟ_F}\='tD]+;M_hm'ۿwWt+GrJl'gMvU6fBc'jhThfH@@@|T4T(^'vo9=>rkEרY[u; q2+T6_ )T*UbWQ寬(3RoĤЕ.ҫIb    q'@G<MIjrVs,o%{nבu35l4Q޺l}j^sϷR YdJPUL[ŊFv@_?#+ 3KeϜžv=nhVܯJ~{$X=U.D_}n9}=    P|Txq(kY   ~ |0"@@@@ )|$=A R@@@ oQC@@@ ࣲ9        `)@% #       -@Ges<@@@@@@@@R e>u&٦ .Bˢ;        )𑚼@@@@@@@_MI@@@@@@@P>*@@@@@@@@ߔ@@@@@@@@  ByI@@@@@@@_ e>z8pxe͚5k"        |I"        [        @ ',"       n>#       ) @G $@E O-Z|׮]%33S͛        # P Jkiiaǿk嗿^Z+ @@@@@@@'@GS}/\/ǧT٪wѣnݺd֭2|0q}oٲEy˘A@@@@@@HɳO#2`碢"CDNNNJ! ~=\|RF CQRR"ӦM[o5ϖx@ZjXw^y,c@@@@@@@ 9|$=gIֽ֭=曁DH{5jɤeɒ%ĉf}{Ҽys]aay2cƌ@@@@@@@bX&zH.S]V.lQduUt# 64E2eqqG2n8iڴg$@@@@@@@>7=nSԁ2o޼㶌YdaX.BS\eϞ= nKjժ%LNPl       YURo&M &xs='vwS:       `/@)<裢{رcwꩧϟ/w 4YrBo1#GQFI-̐G{ʺug5kDr)mٲEhO?-XO>C߾}m۶RPP [n^zBzؙ}ԬYS䬳:2o90ŋkI06G@@@@@@K$}L|iРԨQ#Esʍ7^dPÇ7"a+Ղ}Lj=><fHs=7ҪI&IVP/_.sfo"_^;xW?lXB{S~_7q́ab~       nWa{: 9997CJFF9 dڵzIII\~Auvmr-=*v2j4nXڴicz|+sss .0۹ "33HNSoϞ=&ϵkזN8!Pm۶ɕW^iֻo31n,2eJc[$67m4@#:Ʉ       l@IDAT>NC|2nܸ@w}|'ʰaLZ:@㭷 oK^І!Cr y[̌3̰0|acnvѽBϖ?R~}^2g/Vw\vef7ߔGyJs'N:Hqq 8C        ~(<9Mer(<3ƬC⋁|ͦ'4- tŀ[=Lc=*k{SO=%sYS~y1=wvyG0Jǎ `.       |SZ~,ʊ;Xd_*.%ࣼ 1?3뮻b? ;&E1 @@@@@@@I*x6 X+墋.mJӦMF:uQXX(z; jGGh=       p|Qϣ7tz뭒|s>|MyGbևhL8Q:t.ѐX      T!9s4h:''G̙#7oderYg||/~w}x<ryM&/C !      X a X|3Fx[òyu]gq<]qb N:H_H]x^b?@@@@@@@K>,+bwۀ{LFaW_m<`*7plذAtO&mV>#YڵKc#77W.uU{9iѢY^>z%<4kLJJJo:4 ~A[kʒRo+Nq{ø|rwƹ'!      -@ߢ>gpCꠧEɼydʕ'VZr)Ԯ];6lI~~̞=;h]NN<e&M2(,,!C;3wq|g}&wu*UI>f `?OrEzkVoQw?LSŢߴibj@@@@@@@ ࣢d-ҵ XЇ?!gqFXjtݬ1|[nA%cƌ ,3GK.$8;v,_<=[CcǎoHF̊ ;${ѽiԩӟ!      TN=f< ݻƋ/HI&ן}ْn=zTdŊ&X'?Yx衇ꙹs ZQΛ u]X+C ~f>H3~:0,ӣ*(f(/ܬ`ҥtO7CtTPP`z3gNX       P|T0pׁgy^eŦwdu۶mɓ'{M"'?Sʈ#Cʌ3Cq=\0ԢEݻwɇ~X        @rH;GE )w}\s5fuV>|xX^Z7o,eڴie       $Osd"ЧO1x`G rJ2e7.)y       RXdIIJCt       @ r       D #*+@@@@@@@@'@G;'@@@@@@@*@GTV"       UOwN        UY0       )q$        pLc!       )!2_ 8Р>Ӳf͚&        |-Jz        @ Q$       -@ߢ       T L        |-Jz        @ Q|ݤϥ6o_!KSQ"G@EW"v^ R$YJP zM۞^~[?G@@@8>H;M+LM{N6y5EK? .Ҭyְ\5EڞB]15ETSjk{@@@@ >QbԨQKN;VˬÅrnm-dگ7i Kkh *A<}M$ݠr7ڷi䕻ޯ$s~%K:KD/iZ#Œz%/~Ҳׅ<+_BJ>Ny>~y:WdWVC7:\?㱭sxoǓdlSd*o<Ε٠a^ޤy{pfߺYUe_5k&YYYҨQ#9|۷Ov!EEElW^KKKeӦM1 /"   `+@`o93G[?M7,oxC/͕9i35xA6g6,,=c/m3rmn=93]׊FAufoQXTU*e3θ]2   %@_ #S#.![59^|X/Ab7ܭ>fzM>rME Bt&E^W+deK[Vtٳq,y﮸ި՟x2^7TdWVs7]?-e?ik׮-#F0zL蠎BS>{fH:   x ËZS?KNmU;M_~reNIX/Z׃ԭT 2h'\h,[~h MUj/U'Cja5K"SU?"]E_e*LSjc_g,ۊǻoo2ǬM}֩^59wIf̺]ĥXS6'J˟5kɑC2-vKK޽-SN|sr)e>*XffԪU+fz4mԬ۸qL:d?bY   @% QI~ڷr#k<1i>"X/@:#pfIuHTlUj/_O`E58{ud몚/׶6OFǏw^oqҢׅ{s`EmQflgeעuNˁeիEgtܹtJСC{X[nɓ':{   @$>"TeV+JUsW/Y$aK@Hq.)عFr)y,S/i4"Z$ ٿu䭜woÖ͠1&m?#ݠkkLCE|h;h,\%uRo\<(zh=:E}i0OMGx}dߦrt>RA3+;3 Zƚy+QNIIF)}]IiirsK>TcWϱGδiai4j[]i+r({BkYJALJCeߖ%e[=98SVvrPvo,歑M^ 3sqnrdu)HaZ)bqҍurzurp&uݦߓ׶;W}^6GKvs>jx9rJq#T1~~mbd)kWoS]ο:N>:FyZ~t~L^?8zqׯ5jՕΧ" 6=- ;W~$.g%mNlc,~q}~s\)߾rWF" w z?VMKeoWG7ʑ|ֿ@BD[3u2oWΏVOl]w9NÄع+io;i3-ٟy v'_/NvؾzADuG].h~j<Fy}-ߵ~MdهR_.҂DΟk{ov=ޠy!V~U~L_gS۫zQ4䭖iYVé vCb ]v="bmOV=[]p%gUmL[ gl]f^z}jC׈ZD믓N:?t~7u)sͫ}\;-XMfpY76 8~fi x]cKoyYء;,zgq ɂA>y睨rUW;tѣGؼyi٦^z2bN/Ӂ[lqVaÆI.]믿ZOŻw>Lt#ѦN8AtuGI~tjml+   *@G -]jr_}6.gn/Uսr_Jk(Iw=;Y//Jְ P_4lk~_n=Azf5_ Wq7::ivi>*{_%ݛDuIod>TPv塗u]|c't2/ l3[NF#?篭G=<:'R{~U{w~lo v9u;O/ן5gxy%4ڪ";~y{=cT,30BApAO[T"k&_<׀%Kȼy"GЀqN3ʽ~0aB76myg,**2oS b/..6iժfAD4Et/͛7Wc{㏃q|x-PA@@@ HVCȏ5 zX/]!/˛l^4i?${رC y,[2ްY/~ jȈyG7)5K7QUúm]ݪ'& n g<=%$M~J>WcAAn?!:t͒u6՘{r"m,`gԟMЄ>֖%Ț6j[ }7 --z^`{}ߨ'Ԑf}wC5@UJExW}.}\afԴݻT/ͼE9aauugk_ 4{F)TC,7t*ФJh?.oa`^t.s^wb1g{?:`v*MO: fG `nNަz3;I7o9>BMFoNu -{csqiӂ7,ۖH6=~u|>m׏Z??iUvm?D϶'uycz}a{q;]}vp@rY2ީeQ=9yɑ&qO@C `y^ ب͉rT Xѓmhݺ@={F 'ڵkȑ#E/B6mds9aYn*f2 ,E–&^zi{OtP3E R~'=^@@@H5>Re..zVG_E͹uwwiO}nz6"EA0FQ 9/^@rz^O+'=&ۗN `zXJܓn4 Z N@wP ѯU+YwE[UWM^{/j4{XF۶Z$g%U)9rHf=IX1>?|eT}ePWOɖ Zߤ)ޤj-mK> [8uAiBvgnl_m~ (YNR'A 9UmZ&g{?]7Փ;ĜC)낲FߨAC$Z~z|>'ɨ?ĮmwmiC.|,e[{}p3e'|Oz)w$Zm__[֡D{/uZOo"g[~wy\1~[4Q=ߦ0\:Ѐgc~TKEW,[e?z0۞R\Gf+aC` p]L<+CV=KiiICVzj>}zҺX d=wN-ZFtGeݺumN9ѣYW_INNN`uw- Af@@@H1>ROҴPۥJv*jmNts\ikqww:OԸ $Rz9ƷlĮr .9S1ۺt/!GgzspIyMO?2H+ox ߑSp?RMFvv/uK޿G2+,zm M4QLl MN>lϟ}2Î߸IﲲsiöX櫿k)>6 ?ߘE#U԰So9/]ƪ{ݶPC*̓? ƽ _D}k2?Oycsy`"e=$VG&guĺ';u(KsV2.3wD$'W)vDb]za{?5kÌkYzo߼9lWwW; PAʂ*u}=5j*FP!ܓV%Re]c…J~۶ms57C2| vg#7n,{,O{HŽ;ʙgiΘ1CVZe@@@ #NV#5k5=f~Იm0s9Pv9~[Y~v:P0o{U|Ҥ 3}ŧu`~jHWnE1ҍ Ը6;ߣ X~=:a1NWKs| UvJjw~]w6m;1/'4]ke73)z/싋v/Ga>2goߖ_5l-ze~CN,]#M keCDjp lflS5yo5b|x9c$rĪ?NyRXwDH{dן X/Vz6<_T?됗KsdG0u)wk"X~?qz~a{xU'CP:YՐϚoS#9+'( ߿xANj歷ޒ"<0to1|*ݻwdA9m .A2b4[N0!0̌{Wҭ[76pG=aS~wG@@@ HN I jujXmNvh[8c!98_}xl#^fw:^{ pKs|w:Gw7DoZSGs/c}a#@9{HFCU=Ӆ~mO'# /uX .!] ?lH~,ubܿ?^_S!_<^kDÇ[#yy?6?<ݥRm>?篭W"^>?]_^꟟'#?b՟D>-?Ͽ37_9Wׯ2DۯYslC;c%r({XRM;*"t ;Z0hР0/D,sh@yE6ހN;MvHC駟*fܹ&ŝ֕W^)z(=u֙^ImZj%'=   @U&4jO_T׿4:ȡ:1$ʰtbrܿ\ ~mGepy<^1~xͷޯ~vW5vtߦaE;#lǐU1C1zh˗ٳCr]kuʦM/M6t¨ܿ|^E;7j(0LNNWhYYYfΝ;壏> ۿCrYg~?,@@@@ 'yJᄐZ_e˂q+m~:FNHKsVNl_w48TjԬe Z_8E/Cu2ƶ6N;9/_$x`P͸~Z{uy[H 6@{](=/x^z).BϩGd7#|tShP#֫gdB"d4AלʺHa{/U(2ɑNkvׅ1WmU Jgn74f򙅮?hw^D'?l?wP69?c.k|u|>mLοS_gϯ?>8( |"_ǝDgǾy[jԿ%e?aI7|f7U\֭=|:=l֯__ SEɂA1#F~---?ѣGߪULO˖- ʂK=7|czpٳ 2ļ]p|mC>~   UP_( ѻRW5PY/Rw.;#R dςn|h鞜g9 kj7F#K_>]z_4xZvTOvaA\S8Y?}\X mmuVm{//e/du(Rkه%ZyS/nKO ߕSRަ5T^:_7YkMjԨ%}Y S,MRsu`3ct31TiiX2j׭~7R2t#>lϟ>ȆY/ˆ/gF =԰G{^A~m/. nJī5jk5Oia"8i;NmlΟk{[H=u?WuY)M)m5 H{dSu9ezXzV7>oI*d[l?νE|bMg*lƳeVS~iѻwލsͼǦ9ZgÖ^W߹~yLFMuʿ?GߙOycW//wkNWKvd c'Mk"CՐ2vnYLz(c_m͢0uT=l}i֬VСCpL4 .={>tRCԩSGF) 40'O,7jR/Խ =㥸ؼjժeI233Ͳ{lcǎ&Y_]t4mZ)Ï<   $@G>[UbOհz'MT0At^r.ۼP}W_ZDh)ܕczi8S"ΐ-->jgNꡠnF6&Zzy9VcӼe)$A l7PZج hm{wmͿΌ z@q/)P1v׫0 7F=et`&U˝?ZM 8%n Xs,腙HQOk@߾-{7R/S J2[1X,) 2 \lu2=.zXZ8ϕ=E п9:}]sU<{T3tP=]dnX9\[ojtE_Sv+'^]J-~vf6{KeE7H :3l_kg{?UWmK?=93nfҴi3_/Uckq)Næꀼ8Y1m_a-pZu"h&_]D?khG:mSkku|>m?uǦ:uOz~쟬é?km~yKo˾Js,*.cg lfz\؛+ ZKWNf]j"ƟhӦz?~ġTYڵvifѡCL/۶m3ݻw.] KϽ}-%%%fp,:c„ "zŋ(һwoö)R@}.3Cm֬Y#7n{J&MO>W)R;^+   (@G>kF% %#V{7Z!8ʛK'ܯ2gmDZwٓ*@!=l{܅?S e_u~ {Q ɶ֏Z>^ڲ~Yi\r.74rTO?Z>F~Ksʾ=TSvQ㵻.g)I;wH7׉x|׸U߶Dv̒Eq#|6e,[9VuӞs^ ],NM *\J` >(A'vzW=z|-oU'Xwq#Łx$9ׯCY?W7|8 zʽ(0oS~'&`tbꠢ6fɪ?6`æH+sozh=޶lmSv5ݯ߹~68';_g2oé?6_g[~s=/z_ߙ: trV=KdKG.==N sNLͅM.<8PrW."mtA3ˮ]V =C4駲}m ɑ*h}z/6pѽ~L81wguOtކHz; @@@H1> ӽ(tWm9j(/ు8tK+ejܟd,m_ NYmD8~:0yZfRtcA˝7~4/*I׋R[x&@ۓƚa&tjN -Rz1CdwW5QuDO QC< {t8妠󯿰ݷy*_-_̸V лx= Ԑ-Ks~UNWʹ~ U i1^]Kn=hwV=5W+?P]ÝN!~eǝy MpM}UBzh~5WAVNd=طE%ۗ~^"ĪID=l6EO [,]_'Z ĺ eϛ.k q'/h 3Ǎ0Gů%Nݯ׫U:y??^6g[~;jr㞏_o<7g>/78ս_Zqح Y*KA9˔ =z~^@{ ٽXς9#ڵ3o/_.g^fvсz/dӦMf. N:aX座=Dt{i>{(Ç7˧Mfz1o\ߕ   UW?=m|/27E*B7x;13K&%Yn_5l!:/mJT׺Pxo3:ak%@2u63[AտRc$Z=Aj` ڔ}$Mu)}h)ر:tCfnl5Jn0rzr]R;e`^ץ:!?)cHiҲDʦQCq?w::pᮄ}:Dz>OVvC˛jϿ۾OekD*G"y~%]Mv?-޷~vg V<)TÇF&tazIDAT}FF4j &*sҁ:0=zx&oÆ E|`/S2%   6z^S :@TzؽFҡTQ4vdKW m0RUd_Y7Pf@EjI+] oWU|(YE@@@ G_ShN^WfղlObSkבu35l4Q޺l}j^sϷR YdJPUL[ŊFv@TϿms>IZ    @ Q$0)j:w"Y1Ri{~[VU9eG@ ʏ   &@G1q nܷEo]*;W|,wKFr~T'![=t)% DG$!   UW*tnθsj YA@@@~rXyX   TAω@@@H>@@@        )&@G0       A@@@@@@@@RL e>u&نw…RXXbd@@@@@@@G e>).        /@GCJ       Ljv).       )       T3>         )q뭷Jݍo!gN}}J       x          #@@@@@@@ @4vA@@@@@@@)@G296        Ah       $Sdsl@@@@@@@@@@@@@@@H        ><         L>ϱ@@@@@@@@|x@c@@@@@@@@ |$Sc#       .        @2H>F@@@@@@@<]@@@@@@@@d dɓe,Xq9 @@@@@@@@E %>yV@@@@@@@Hy>RR@@@@@@@n)1j(i׮]SO܆ @@@@@@@@THTF&        |IZ        @%Q @@@@@@@S?5I @@@@@@@>*C        ~ &i!        @G% s@@@@@@@@O>$-@@@@@@@@d       )@       T!@@@@@@@@?S@@@@@@@@J 9        ~j        P |T2@@@@@@@@ OMB@@@@@@@*AJ@        |IZ        @%Q @@@@@@@S?5I @@@@@@@>*C        ~ &i!        @G% s@@@@];@5& @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @d @ @ @ @R@Qj"@ @ @ @^UyIENDB`golang-github-olekukonko-ll-0.0.9/concat.go000066400000000000000000000275161504320775200206510ustar00rootroot00000000000000package ll import ( "fmt" "github.com/olekukonko/ll/lx" "reflect" "strconv" "strings" "unsafe" ) const ( maxRecursionDepth = 20 // Maximum depth for recursive type handling to prevent stack overflow nilString = "" // String representation for nil values unexportedString = "" // String representation for unexported fields ) // Concat efficiently concatenates values without a separator using the default logger. // It converts each argument to a string and joins them directly, optimizing for performance // in logging scenarios. Thread-safe as it does not modify shared state. // Example: // // msg := ll.Concat("Hello", 42, true) // Returns "Hello42true" func Concat(args ...any) string { return concat(args...) } // ConcatSpaced concatenates values with a space separator using the default logger. // It converts each argument to a string and joins them with spaces, suitable for log message // formatting. Thread-safe as it does not modify shared state. // Example: // // msg := ll.ConcatSpaced("Hello", 42, true) // Returns "Hello 42 true" func ConcatSpaced(args ...any) string { return concatSpaced(args...) } // ConcatAll concatenates elements with a separator, prefix, and suffix using the default logger. // It combines before, main, and after arguments with the specified separator, optimizing memory // allocation for logging. Thread-safe as it does not modify shared state. // Example: // // msg := ll.ConcatAll(",", []any{"prefix"}, []any{"suffix"}, "main") // // Returns "prefix,main,suffix" func ConcatAll(sep string, before, after []any, args ...any) string { return concatenate(sep, before, after, args...) } // concat efficiently concatenates values without a separator. // It converts each argument to a string and joins them directly, optimizing for performance // in logging scenarios. Used internally by Concat and other logging functions. // Example: // // msg := concat("Hello", 42, true) // Returns "Hello42true" func concat(args ...any) string { return concatWith("", args...) } // concatSpaced concatenates values with a space separator. // It converts each argument to a string and joins them with spaces, suitable for formatting // log messages. Used internally by ConcatSpaced. // Example: // // msg := concatSpaced("Hello", 42, true) // Returns "Hello 42 true" func concatSpaced(args ...any) string { return concatWith(lx.Space, args...) } // concatWith concatenates values with a specified separator using optimized type handling. // It builds a string from arguments, handling various types efficiently (strings, numbers, // structs, etc.), and is used by concat and concatSpaced for log message construction. // Thread-safe as it does not modify shared state. // Example: // // msg := concatWith(",", "Hello", 42, true) // Returns "Hello,42,true" func concatWith(sep string, args ...any) string { switch len(args) { case 0: return "" case 1: return concatToString(args[0]) } var b strings.Builder b.Grow(concatEstimateArgs(sep, args)) for i, arg := range args { if i > 0 { b.WriteString(sep) } concatWriteValue(&b, arg, 0) } return b.String() } // concatenate concatenates elements with separators, prefixes, and suffixes efficiently. // It combines before, main, and after arguments with the specified separator, optimizing // memory allocation for complex log message formatting. Used internally by ConcatAll. // Example: // // msg := concatenate(",", []any{"prefix"}, []any{"suffix"}, "main") // // Returns "prefix,main,suffix" func concatenate(sep string, before []any, after []any, args ...any) string { totalLen := len(before) + len(after) + len(args) switch totalLen { case 0: return "" case 1: switch { case len(before) > 0: return concatToString(before[0]) case len(args) > 0: return concatToString(args[0]) default: return concatToString(after[0]) } } var b strings.Builder b.Grow(concatEstimateTotal(sep, before, after, args)) // Write before elements concatWriteGroup(&b, sep, before) // Write main arguments if len(before) > 0 && len(args) > 0 { b.WriteString(sep) } concatWriteGroup(&b, sep, args) // Write after elements if len(after) > 0 && (len(before) > 0 || len(args) > 0) { b.WriteString(sep) } concatWriteGroup(&b, sep, after) return b.String() } // concatWriteGroup writes a group of arguments to a strings.Builder with a separator. // It handles each argument by converting it to a string, used internally by concatenate // to process before, main, or after groups in log message construction. // Example: // // var b strings.Builder // concatWriteGroup(&b, ",", []any{"a", 42}) // Writes "a,42" to b func concatWriteGroup(b *strings.Builder, sep string, group []any) { for i, arg := range group { if i > 0 { b.WriteString(sep) } concatWriteValue(b, arg, 0) } } // concatToString converts a single argument to a string efficiently. // It handles common types (string, []byte, fmt.Stringer) with minimal overhead and falls // back to fmt.Sprint for other types. Used internally by concat and concatenate. // Example: // // s := concatToString("Hello") // Returns "Hello" // s := concatToString([]byte{65, 66}) // Returns "AB" func concatToString(arg any) string { switch v := arg.(type) { case string: return v case []byte: return *(*string)(unsafe.Pointer(&v)) case fmt.Stringer: return v.String() case error: return v.Error() default: return fmt.Sprint(v) } } // concatEstimateTotal estimates the total string length for concatenate. // It calculates the expected size of the concatenated string, including before, main, and // after arguments with separators, to preallocate the strings.Builder capacity. // Example: // // size := concatEstimateTotal(",", []any{"prefix"}, []any{"suffix"}, "main") // // Returns estimated length for "prefix,main,suffix" func concatEstimateTotal(sep string, before, after, args []any) int { size := 0 if len(before) > 0 { size += concatEstimateArgs(sep, before) } if len(args) > 0 { if size > 0 { size += len(sep) } size += concatEstimateArgs(sep, args) } if len(after) > 0 { if size > 0 { size += len(sep) } size += concatEstimateArgs(sep, after) } return size } // concatEstimateArgs estimates the string length for a group of arguments. // It sums the estimated sizes of each argument plus separators, used by concatEstimateTotal // and concatWith to optimize memory allocation for log message construction. // Example: // // size := concatEstimateArgs(",", []any{"hello", 42}) // Returns estimated length for "hello,42" func concatEstimateArgs(sep string, args []any) int { if len(args) == 0 { return 0 } size := len(sep) * (len(args) - 1) for _, arg := range args { size += concatEstimateSize(arg) } return size } // concatEstimateSize estimates the string length for a single argument. // It provides size estimates for various types (strings, numbers, booleans, etc.) to // optimize strings.Builder capacity allocation in logging functions. // Example: // // size := concatEstimateSize("hello") // Returns 5 // size := concatEstimateSize(42) // Returns ~2 func concatEstimateSize(arg any) int { switch v := arg.(type) { case string: return len(v) case []byte: return len(v) case int: return concatNumLen(int64(v)) case int64: return concatNumLen(v) case int32: return concatNumLen(int64(v)) case int16: return concatNumLen(int64(v)) case int8: return concatNumLen(int64(v)) case uint: return concatNumLen(uint64(v)) case uint64: return concatNumLen(v) case uint32: return concatNumLen(uint64(v)) case uint16: return concatNumLen(uint64(v)) case uint8: return concatNumLen(uint64(v)) case float64: return 24 // Max digits for float64 case float32: return 16 // Max digits for float32 case bool: if v { return 4 // "true" } return 5 // "false" case fmt.Stringer: return 16 // Conservative estimate default: return 16 // Default estimate } } // concatNumLen estimates the string length for a signed or unsigned integer. // It returns a conservative estimate (20 digits) for int64 or uint64 values, including // a sign for negative numbers, used by concatEstimateSize for memory allocation. // Example: // // size := concatNumLen(int64(-123)) // Returns 20 // size := concatNumLen(uint64(123)) // Returns 20 func concatNumLen[T int64 | uint64](v T) int { if v < 0 { return 20 // Max digits for int64 + sign } return 20 // Max digits for uint64 } // concatWriteValue writes a formatted value to a strings.Builder with recursion depth tracking. // It handles various types (strings, numbers, structs, slices, etc.) and prevents infinite // recursion by limiting depth. Used internally by concatWith and concatWriteGroup for log // message formatting. // Example: // // var b strings.Builder // concatWriteValue(&b, "hello", 0) // Writes "hello" to b // concatWriteValue(&b, []int{1, 2}, 0) // Writes "[1,2]" to b func concatWriteValue(b *strings.Builder, arg any, depth int) { if depth > maxRecursionDepth { b.WriteString("...") return } if arg == nil { b.WriteString(nilString) return } if s, ok := arg.(fmt.Stringer); ok { b.WriteString(s.String()) return } switch v := arg.(type) { case string: b.WriteString(v) case []byte: b.Write(v) case int: b.WriteString(strconv.FormatInt(int64(v), 10)) case int64: b.WriteString(strconv.FormatInt(v, 10)) case int32: b.WriteString(strconv.FormatInt(int64(v), 10)) case int16: b.WriteString(strconv.FormatInt(int64(v), 10)) case int8: b.WriteString(strconv.FormatInt(int64(v), 10)) case uint: b.WriteString(strconv.FormatUint(uint64(v), 10)) case uint64: b.WriteString(strconv.FormatUint(v, 10)) case uint32: b.WriteString(strconv.FormatUint(uint64(v), 10)) case uint16: b.WriteString(strconv.FormatUint(uint64(v), 10)) case uint8: b.WriteString(strconv.FormatUint(uint64(v), 10)) case float64: b.WriteString(strconv.FormatFloat(v, 'f', -1, 64)) case float32: b.WriteString(strconv.FormatFloat(float64(v), 'f', -1, 32)) case bool: if v { b.WriteString("true") } else { b.WriteString("false") } default: val := reflect.ValueOf(arg) if val.Kind() == reflect.Ptr { if val.IsNil() { b.WriteString(nilString) return } val = val.Elem() } switch val.Kind() { case reflect.Slice, reflect.Array: concatFormatSlice(b, val, depth) case reflect.Struct: concatFormatStruct(b, val, depth) default: fmt.Fprint(b, v) } } } // concatFormatSlice formats a slice or array for logging. // It writes the elements in a bracketed, comma-separated format, handling nested types // recursively with depth tracking. Used internally by concatWriteValue for log message formatting. // Example: // // var b strings.Builder // val := reflect.ValueOf([]int{1, 2}) // concatFormatSlice(&b, val, 0) // Writes "[1,2]" to b func concatFormatSlice(b *strings.Builder, val reflect.Value, depth int) { b.WriteByte('[') for i := 0; i < val.Len(); i++ { if i > 0 { b.WriteByte(',') } concatWriteValue(b, val.Index(i).Interface(), depth+1) } b.WriteByte(']') } // concatFormatStruct formats a struct for logging. // It writes the struct’s exported fields in a bracketed, name:value format, handling nested // types recursively with depth tracking. Unexported fields are represented as "". // Used internally by concatWriteValue for log message formatting. // Example: // // var b strings.Builder // val := reflect.ValueOf(struct{ Name string }{Name: "test"}) // concatFormatStruct(&b, val, 0) // Writes "[Name:test]" to b func concatFormatStruct(b *strings.Builder, val reflect.Value, depth int) { typ := val.Type() b.WriteByte('[') first := true for i := 0; i < val.NumField(); i++ { field := typ.Field(i) fieldValue := val.Field(i) if !first { b.WriteString("; ") } first = false b.WriteString(field.Name) b.WriteByte(':') if !fieldValue.CanInterface() { b.WriteString(unexportedString) continue } concatWriteValue(b, fieldValue.Interface(), depth+1) } b.WriteByte(']') } golang-github-olekukonko-ll-0.0.9/conditional.go000066400000000000000000000333111504320775200216730ustar00rootroot00000000000000package ll // Conditional enables conditional logging based on a boolean condition. // It wraps a logger with a condition that determines whether logging operations are executed, // optimizing performance by skipping expensive operations (e.g., field computation, message formatting) // when the condition is false. The struct supports fluent chaining for adding fields and logging. type Conditional struct { logger *Logger // Associated logger instance for logging operations condition bool // Whether logging is allowed (true to log, false to skip) } // If creates a conditional logger that logs only if the condition is true. // It returns a Conditional struct that wraps the logger, enabling conditional logging methods. // This method is typically called on a Logger instance to start a conditional chain. // Thread-safe via the underlying logger’s mutex. // Example: // // logger := New("app").Enable() // logger.If(true).Info("Logged") // Output: [app] INFO: Logged // logger.If(false).Info("Ignored") // No output func (l *Logger) If(condition bool) *Conditional { return &Conditional{logger: l, condition: condition} } // IfOne creates a conditional logger that logs only if all conditions are true. // It evaluates a variadic list of boolean conditions, setting the condition to true only if // all are true (logical AND). Returns a new Conditional with the result. Thread-safe via the // underlying logger. // Example: // // logger := New("app").Enable() // logger.IfOne(true, true).Info("Logged") // Output: [app] INFO: Logged // logger.IfOne(true, false).Info("Ignored") // No output func (cl *Conditional) IfOne(conditions ...bool) *Conditional { result := true // Check each condition; set result to false if any is false for _, cond := range conditions { if !cond { result = false break } } return &Conditional{logger: cl.logger, condition: result} } // IfAny creates a conditional logger that logs only if at least one condition is true. // It evaluates a variadic list of boolean conditions, setting the condition to true if any // is true (logical OR). Returns a new Conditional with the result. Thread-safe via the // underlying logger. // Example: // // logger := New("app").Enable() // logger.IfAny(false, true).Info("Logged") // Output: [app] INFO: Logged // logger.IfAny(false, false).Info("Ignored") // No output func (cl *Conditional) IfAny(conditions ...bool) *Conditional { result := false // Check each condition; set result to true if any is true for _, cond := range conditions { if cond { result = true break } } return &Conditional{logger: cl.logger, condition: result} } // Fields starts a fluent chain for adding fields using variadic key-value pairs, if the condition is true. // It returns a FieldBuilder to attach fields, skipping field processing if the condition is false // to optimize performance. Thread-safe via the FieldBuilder’s logger. // Example: // // logger := New("app").Enable() // logger.If(true).Fields("user", "alice").Info("Logged") // Output: [app] INFO: Logged [user=alice] // logger.If(false).Fields("user", "alice").Info("Ignored") // No output, no field processing func (cl *Conditional) Fields(pairs ...any) *FieldBuilder { // Skip field processing if condition is false if !cl.condition { return &FieldBuilder{logger: cl.logger, fields: nil} } // Delegate to logger’s Fields method return cl.logger.Fields(pairs...) } // Field starts a fluent chain for adding fields from a map, if the condition is true. // It returns a FieldBuilder to attach fields from a map, skipping processing if the condition // is false. Thread-safe via the FieldBuilder’s logger. // Example: // // logger := New("app").Enable() // logger.If(true).Field(map[string]interface{}{"user": "alice"}).Info("Logged") // Output: [app] INFO: Logged [user=alice] // logger.If(false).Field(map[string]interface{}{"user": "alice"}).Info("Ignored") // No output func (cl *Conditional) Field(fields map[string]interface{}) *FieldBuilder { // Skip field processing if condition is false if !cl.condition { return &FieldBuilder{logger: cl.logger, fields: nil} } // Delegate to logger’s Field method return cl.logger.Field(fields) } // Info logs a message at Info level with variadic arguments if the condition is true. // It concatenates the arguments with spaces and delegates to the logger’s Info method if the // condition is true. Skips processing if false, optimizing performance. Thread-safe via the // logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Info("Action", "started") // Output: [app] INFO: Action started // logger.If(false).Info("Action", "ignored") // No output func (cl *Conditional) Info(args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Info method cl.logger.Info(args...) } // Infof logs a message at Info level with a format string if the condition is true. // It formats the message using the provided format string and arguments, delegating to the // logger’s Infof method if the condition is true. Skips processing if false, optimizing performance. // Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Infof("Action %s", "started") // Output: [app] INFO: Action started // logger.If(false).Infof("Action %s", "ignored") // No output func (cl *Conditional) Infof(format string, args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Infof method cl.logger.Infof(format, args...) } // Debug logs a message at Debug level with variadic arguments if the condition is true. // It concatenates the arguments with spaces and delegates to the logger’s Debug method if the // condition is true. Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable().Level(lx.LevelDebug) // logger.If(true).Debug("Debugging", "mode") // Output: [app] DEBUG: Debugging mode // logger.If(false).Debug("Debugging", "ignored") // No output func (cl *Conditional) Debug(args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Debug method cl.logger.Debug(args...) } // Debugf logs a message at Debug level with a format string if the condition is true. // It formats the message and delegates to the logger’s Debugf method if the condition is true. // Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable().Level(lx.LevelDebug) // logger.If(true).Debugf("Debug %s", "mode") // Output: [app] DEBUG: Debug mode // logger.If(false).Debugf("Debug %s", "ignored") // No output func (cl *Conditional) Debugf(format string, args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Debugf method cl.logger.Debugf(format, args...) } // Warn logs a message at Warn level with variadic arguments if the condition is true. // It concatenates the arguments with spaces and delegates to the logger’s Warn method if the // condition is true. Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Warn("Warning", "issued") // Output: [app] WARN: Warning issued // logger.If(false).Warn("Warning", "ignored") // No output func (cl *Conditional) Warn(args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Warn method cl.logger.Warn(args...) } // Warnf logs a message at Warn level with a format string if the condition is true. // It formats the message and delegates to the logger’s Warnf method if the condition is true. // Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued // logger.If(false).Warnf("Warning %s", "ignored") // No output func (cl *Conditional) Warnf(format string, args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Warnf method cl.logger.Warnf(format, args...) } // Error logs a message at Error level with variadic arguments if the condition is true. // It concatenates the arguments with spaces and delegates to the logger’s Error method if the // condition is true. Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Error("Error", "occurred") // Output: [app] ERROR: Error occurred // logger.If(false).Error("Error", "ignored") // No output func (cl *Conditional) Error(args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Error method cl.logger.Error(args...) } // Errorf logs a message at Error level with a format string if the condition is true. // It formats the message and delegates to the logger’s Errorf method if the condition is true. // Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred // logger.If(false).Errorf("Error %s", "ignored") // No output func (cl *Conditional) Errorf(format string, args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Errorf method cl.logger.Errorf(format, args...) } // Stack logs a message at Error level with a stack trace and variadic arguments if the condition is true. // It concatenates the arguments with spaces and delegates to the logger’s Stack method if the // condition is true. Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Stack("Critical", "error") // Output: [app] ERROR: Critical error [stack=...] // logger.If(false).Stack("Critical", "ignored") // No output func (cl *Conditional) Stack(args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Stack method cl.logger.Stack(args...) } // Stackf logs a message at Error level with a stack trace and a format string if the condition is true. // It formats the message and delegates to the logger’s Stackf method if the condition is true. // Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [stack=...] // logger.If(false).Stackf("Critical %s", "ignored") // No output func (cl *Conditional) Stackf(format string, args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Stackf method cl.logger.Stackf(format, args...) } // Fatal logs a message at Error level with a stack trace and variadic arguments if the condition is true, // then exits. It concatenates the arguments with spaces and delegates to the logger’s Fatal method // if the condition is true, terminating the program with exit code 1. Skips processing if false. // Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Fatal("Fatal", "error") // Output: [app] ERROR: Fatal error [stack=...], then exits // logger.If(false).Fatal("Fatal", "ignored") // No output, no exit func (cl *Conditional) Fatal(args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Fatal method cl.logger.Fatal(args...) } // Fatalf logs a formatted message at Error level with a stack trace if the condition is true, then exits. // It formats the message and delegates to the logger’s Fatalf method if the condition is true, // terminating the program with exit code 1. Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [stack=...], then exits // logger.If(false).Fatalf("Fatal %s", "ignored") // No output, no exit func (cl *Conditional) Fatalf(format string, args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Fatalf method cl.logger.Fatalf(format, args...) } // Panic logs a message at Error level with a stack trace and variadic arguments if the condition is true, // then panics. It concatenates the arguments with spaces and delegates to the logger’s Panic method // if the condition is true, triggering a panic. Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Panic("Panic", "error") // Output: [app] ERROR: Panic error [stack=...], then panics // logger.If(false).Panic("Panic", "ignored") // No output, no panic func (cl *Conditional) Panic(args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Panic method cl.logger.Panic(args...) } // Panicf logs a formatted message at Error level with a stack trace if the condition is true, then panics. // It formats the message and delegates to the logger’s Panicf method if the condition is true, // triggering a panic. Skips processing if false. Thread-safe via the logger’s log method. // Example: // // logger := New("app").Enable() // logger.If(true).Panicf("Panic %s", "error") // Output: [app] ERROR: Panic error [stack=...], then panics // logger.If(false).Panicf("Panic %s", "ignored") // No output, no panic func (cl *Conditional) Panicf(format string, args ...any) { // Skip logging if condition is false if !cl.condition { return } // Delegate to logger’s Panicf method cl.logger.Panicf(format, args...) } golang-github-olekukonko-ll-0.0.9/field.go000066400000000000000000000347601504320775200204640ustar00rootroot00000000000000package ll import ( "fmt" "github.com/olekukonko/ll/lx" "os" "strings" ) // FieldBuilder enables fluent addition of fields before logging. // It acts as a builder pattern to attach key-value pairs (fields) to log entries, // supporting structured logging with metadata. The builder allows chaining to add fields // and log messages at various levels (Info, Debug, Warn, Error, etc.) in a single expression. type FieldBuilder struct { logger *Logger // Associated logger instance for logging operations fields map[string]interface{} // Fields to include in the log entry as key-value pairs } // Logger creates a new logger with the builder’s fields embedded in its context. // It clones the parent logger and copies the builder’s fields into the new logger’s context, // enabling persistent field inclusion in subsequent logs. This method supports fluent chaining // after Fields or Field calls. // Example: // // logger := New("app").Enable() // newLogger := logger.Fields("user", "alice").Logger() // newLogger.Info("Action") // Output: [app] INFO: Action [user=alice] func (fb *FieldBuilder) Logger() *Logger { // Clone the parent logger to preserve its configuration newLogger := fb.logger.Clone() // Initialize a new context map to avoid modifying the parent’s context newLogger.context = make(map[string]interface{}) // Copy builder’s fields into the new logger’s context for k, v := range fb.fields { newLogger.context[k] = v } return newLogger } // Info logs a message at Info level with the builder’s fields. // It concatenates the arguments with spaces and delegates to the logger’s log method, // returning early if fields are nil. This method is used for informational messages. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Info("Action", "started") // Output: [app] INFO: Action started [user=alice] func (fb *FieldBuilder) Info(args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Log at Info level with the builder’s fields, no stack trace fb.logger.log(lx.LevelInfo, lx.ClassText, concatSpaced(args...), fb.fields, false) } // Infof logs a message at Info level with the builder’s fields. // It formats the message using the provided format string and arguments, then delegates // to the logger’s internal log method. If fields are nil, it returns early to avoid logging. // This method is part of the fluent API, typically called after adding fields. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Infof("Action %s", "started") // Output: [app] INFO: Action started [user=alice] func (fb *FieldBuilder) Infof(format string, args ...any) { // Skip logging if fields are nil to prevent invalid log entries if fb.fields == nil { return } // Format the message using the provided arguments msg := fmt.Sprintf(format, args...) // Log at Info level with the builder’s fields, no stack trace fb.logger.log(lx.LevelInfo, lx.ClassText, msg, fb.fields, false) } // Debug logs a message at Debug level with the builder’s fields. // It concatenates the arguments with spaces and delegates to the logger’s log method, // returning early if fields are nil. This method is used for debugging information. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Debug("Debugging", "mode") // Output: [app] DEBUG: Debugging mode [user=alice] func (fb *FieldBuilder) Debug(args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Log at Debug level with the builder’s fields, no stack trace fb.logger.log(lx.LevelDebug, lx.ClassText, concatSpaced(args...), fb.fields, false) } // Debugf logs a message at Debug level with the builder’s fields. // It formats the message and delegates to the logger’s log method, returning early if // fields are nil. This method is used for debugging information that may be disabled in // production environments. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Debugf("Debug %s", "mode") // Output: [app] DEBUG: Debug mode [user=alice] func (fb *FieldBuilder) Debugf(format string, args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Format the message msg := fmt.Sprintf(format, args...) // Log at Debug level with the builder’s fields, no stack trace fb.logger.log(lx.LevelDebug, lx.ClassText, msg, fb.fields, false) } // Warn logs a message at Warn level with the builder’s fields. // It concatenates the arguments with spaces and delegates to the logger’s log method, // returning early if fields are nil. This method is used for warning conditions. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Warn("Warning", "issued") // Output: [app] WARN: Warning issued [user=alice] func (fb *FieldBuilder) Warn(args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Log at Warn level with the builder’s fields, no stack trace fb.logger.log(lx.LevelWarn, lx.ClassText, concatSpaced(args...), fb.fields, false) } // Warnf logs a message at Warn level with the builder’s fields. // It formats the message and delegates to the logger’s log method, returning early if // fields are nil. This method is used for warning conditions that do not halt execution. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued [user=alice] func (fb *FieldBuilder) Warnf(format string, args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Format the message msg := fmt.Sprintf(format, args...) // Log at Warn level with the builder’s fields, no stack trace fb.logger.log(lx.LevelWarn, lx.ClassText, msg, fb.fields, false) } // Error logs a message at Error level with the builder’s fields. // It concatenates the arguments with spaces and delegates to the logger’s log method, // returning early if fields are nil. This method is used for error conditions. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Error("Error", "occurred") // Output: [app] ERROR: Error occurred [user=alice] func (fb *FieldBuilder) Error(args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Log at Error level with the builder’s fields, no stack trace fb.logger.log(lx.LevelError, lx.ClassText, concatSpaced(args...), fb.fields, false) } // Errorf logs a message at Error level with the builder’s fields. // It formats the message and delegates to the logger’s log method, returning early if // fields are nil. This method is used for error conditions that may require attention. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred [user=alice] func (fb *FieldBuilder) Errorf(format string, args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Format the message msg := fmt.Sprintf(format, args...) // Log at Error level with the builder’s fields, no stack trace fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, false) } // Stack logs a message at Error level with a stack trace and the builder’s fields. // It concatenates the arguments with spaces and delegates to the logger’s log method, // returning early if fields are nil. This method is useful for debugging critical errors. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Stack("Critical", "error") // Output: [app] ERROR: Critical error [user=alice stack=...] func (fb *FieldBuilder) Stack(args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Log at Error level with the builder’s fields and a stack trace fb.logger.log(lx.LevelError, lx.ClassText, concatSpaced(args...), fb.fields, true) } // Stackf logs a message at Error level with a stack trace and the builder’s fields. // It formats the message and delegates to the logger’s log method, returning early if // fields are nil. This method is useful for debugging critical errors. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [user=alice stack=...] func (fb *FieldBuilder) Stackf(format string, args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Format the message msg := fmt.Sprintf(format, args...) // Log at Error level with the builder’s fields and a stack trace fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, true) } // Fatal logs a message at Error level with a stack trace and the builder’s fields, then exits. // It constructs the message from variadic arguments, logs it with a stack trace, and terminates // the program with exit code 1. Returns early if fields are nil. This method is used for // unrecoverable errors. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Fatal("Fatal", "error") // Output: [app] ERROR: Fatal error [user=alice stack=...], then exits func (fb *FieldBuilder) Fatal(args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Build the message by concatenating arguments with spaces var builder strings.Builder for i, arg := range args { if i > 0 { builder.WriteString(lx.Space) } builder.WriteString(fmt.Sprint(arg)) } // Log at Error level with the builder’s fields and a stack trace fb.logger.log(lx.LevelError, lx.ClassText, builder.String(), fb.fields, true) // Exit the program with status code 1 os.Exit(1) } // Fatalf logs a formatted message at Error level with a stack trace and the builder’s fields, // then exits. It delegates to Fatal and returns early if fields are nil. This method is used // for unrecoverable errors. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [user=alice stack=...], then exits func (fb *FieldBuilder) Fatalf(format string, args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Format the message and pass to Fatal fb.Fatal(fmt.Sprintf(format, args...)) } // Panic logs a message at Error level with a stack trace and the builder’s fields, then panics. // It constructs the message from variadic arguments, logs it with a stack trace, and triggers // a panic with the message. Returns early if fields are nil. This method is used for critical // errors that require immediate program termination with a panic. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Panic("Panic", "error") // Output: [app] ERROR: Panic error [user=alice stack=...], then panics func (fb *FieldBuilder) Panic(args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Build the message by concatenating arguments with spaces var builder strings.Builder for i, arg := range args { if i > 0 { builder.WriteString(lx.Space) } builder.WriteString(fmt.Sprint(arg)) } msg := builder.String() // Log at Error level with the builder’s fields and a stack trace fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, true) // Trigger a panic with the formatted message panic(msg) } // Panicf logs a formatted message at Error level with a stack trace and the builder’s fields, // then panics. It delegates to Panic and returns early if fields are nil. This method is used // for critical errors that require immediate program termination with a panic. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Panicf("Panic %s", "error") // Output: [app] ERROR: Panic error [user=alice stack=...], then panics func (fb *FieldBuilder) Panicf(format string, args ...any) { // Skip logging if fields are nil if fb.fields == nil { return } // Format the message and pass to Panic fb.Panic(fmt.Sprintf(format, args...)) } // Err adds one or more errors to the FieldBuilder as a field and logs them. // It stores non-nil errors in the "error" field: a single error if only one is non-nil, // or a slice of errors if multiple are non-nil. It logs the concatenated string representations // of non-nil errors (e.g., "failed 1; failed 2") at the Error level. Returns the FieldBuilder // for chaining, allowing further field additions or logging. Thread-safe via the logger’s mutex. // Example: // // logger := New("app").Enable() // err1 := errors.New("failed 1") // err2 := errors.New("failed 2") // logger.Fields("k", "v").Err(err1, err2).Info("Error occurred") // // Output: [app] ERROR: failed 1; failed 2 // // [app] INFO: Error occurred [error=[failed 1 failed 2] k=v] func (fb *FieldBuilder) Err(errs ...error) *FieldBuilder { // Initialize fields map if nil if fb.fields == nil { fb.fields = make(map[string]interface{}) } // Collect non-nil errors and build log message var nonNilErrors []error var builder strings.Builder count := 0 for i, err := range errs { if err != nil { if i > 0 && count > 0 { builder.WriteString("; ") } builder.WriteString(err.Error()) nonNilErrors = append(nonNilErrors, err) count++ } } // Set error field and log if there are non-nil errors if count > 0 { if count == 1 { // Store single error directly fb.fields["error"] = nonNilErrors[0] } else { // Store slice of errors fb.fields["error"] = nonNilErrors } // Log concatenated error messages at Error level fb.logger.log(lx.LevelError, lx.ClassText, builder.String(), nil, false) } // Return FieldBuilder for chaining return fb } // Merge adds additional key-value pairs to the FieldBuilder. // It processes variadic arguments as key-value pairs, expecting string keys. Non-string keys // or uneven pairs generate an "error" field with a descriptive message. Returns the FieldBuilder // for chaining to allow further field additions or logging. // Example: // // logger := New("app").Enable() // logger.Fields("k1", "v1").Merge("k2", "v2").Info("Action") // Output: [app] INFO: Action [k1=v1 k2=v2] func (fb *FieldBuilder) Merge(pairs ...any) *FieldBuilder { // Process pairs as key-value, advancing by 2 for i := 0; i < len(pairs)-1; i += 2 { // Ensure the key is a string if key, ok := pairs[i].(string); ok { fb.fields[key] = pairs[i+1] } else { // Log an error field for non-string keys fb.fields["error"] = fmt.Errorf("non-string key in Merge: %v", pairs[i]) } } // Check for uneven pairs (missing value) if len(pairs)%2 != 0 { fb.fields["error"] = fmt.Errorf("uneven key-value pairs in Merge: [%v]", pairs[len(pairs)-1]) } return fb } golang-github-olekukonko-ll-0.0.9/global.go000066400000000000000000000602471504320775200206400ustar00rootroot00000000000000package ll import ( "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" "os" "sync/atomic" "time" ) // defaultLogger is the global logger instance for package-level logging functions. // It provides a shared logger for convenience, allowing logging without explicitly creating // a logger instance. The logger is initialized with default settings: enabled, Debug level, // flat namespace style, and a text handler to os.Stdout. It is thread-safe due to the Logger // struct’s mutex. var defaultLogger = &Logger{ enabled: true, // Initially enabled level: lx.LevelDebug, // Minimum log level set to Debug namespaces: defaultStore, // Shared namespace store for enable/disable states context: make(map[string]interface{}), // Empty context for global fields style: lx.FlatPath, // Flat namespace style (e.g., [parent/child]) handler: lh.NewTextHandler(os.Stdout), // Default text handler to os.Stdout middleware: make([]Middleware, 0), // Empty middleware chain stackBufferSize: 4096, // Buffer size for stack traces } // Handler sets the handler for the default logger. // It configures the output destination and format (e.g., text, JSON) for logs emitted by // defaultLogger. Returns the default logger for method chaining, enabling fluent configuration. // Example: // // ll.Handler(lh.NewJSONHandler(os.Stdout)).Enable() // ll.Info("Started") // Output: {"level":"INFO","message":"Started"} func Handler(handler lx.Handler) *Logger { return defaultLogger.Handler(handler) } // Level sets the minimum log level for the default logger. // It determines which log messages (Debug, Info, Warn, Error) are emitted. Messages below // the specified level are ignored. Returns the default logger for method chaining. // Example: // // ll.Level(lx.LevelWarn) // ll.Info("Ignored") // No output // ll.Warn("Logged") // Output: [] WARN: Logged func Level(level lx.LevelType) *Logger { return defaultLogger.Level(level) } // Style sets the namespace style for the default logger. // It controls how namespace paths are formatted in logs (FlatPath: [parent/child], // NestedPath: [parent]→[child]). Returns the default logger for method chaining. // Example: // // ll.Style(lx.NestedPath) // ll.Info("Test") // Output: []: INFO: Test func Style(style lx.StyleType) *Logger { return defaultLogger.Style(style) } // NamespaceEnable enables logging for a namespace and its children using the default logger. // It activates logging for the specified namespace path (e.g., "app/db") and all its // descendants. Returns the default logger for method chaining. Thread-safe via the Logger’s mutex. // Example: // // ll.NamespaceEnable("app/db") // ll.Clone().Namespace("db").Info("Query") // Output: [app/db] INFO: Query func NamespaceEnable(path string) *Logger { return defaultLogger.NamespaceEnable(path) } // NamespaceDisable disables logging for a namespace and its children using the default logger. // It suppresses logging for the specified namespace path and all its descendants. Returns // the default logger for method chaining. Thread-safe via the Logger’s mutex. // Example: // // ll.NamespaceDisable("app/db") // ll.Clone().Namespace("db").Info("Query") // No output func NamespaceDisable(path string) *Logger { return defaultLogger.NamespaceDisable(path) } // Namespace creates a child logger with a sub-namespace appended to the current path. // The child inherits the default logger’s configuration but has an independent context. // Thread-safe with read lock. Returns the new logger for further configuration or logging. // Example: // // logger := ll.Namespace("app") // logger.Info("Started") // Output: [app] INFO: Started func Namespace(name string) *Logger { return defaultLogger.Namespace(name) } // Info logs a message at Info level with variadic arguments using the default logger. // It concatenates the arguments with spaces and delegates to defaultLogger’s Info method. // Thread-safe via the Logger’s log method. // Example: // // ll.Info("Service", "started") // Output: [] INFO: Service started func Info(args ...any) { defaultLogger.Info(args...) } // Infof logs a message at Info level with a format string using the default logger. // It formats the message using the provided format string and arguments, then delegates to // defaultLogger’s Infof method. Thread-safe via the Logger’s log method. // Example: // // ll.Infof("Service %s", "started") // Output: [] INFO: Service started func Infof(format string, args ...any) { defaultLogger.Infof(format, args...) } // Debug logs a message at Debug level with variadic arguments using the default logger. // It concatenates the arguments with spaces and delegates to defaultLogger’s Debug method. // Used for debugging information, typically disabled in production. Thread-safe. // Example: // // ll.Level(lx.LevelDebug) // ll.Debug("Debugging", "mode") // Output: [] DEBUG: Debugging mode func Debug(args ...any) { defaultLogger.Debug(args...) } // Debugf logs a message at Debug level with a format string using the default logger. // It formats the message and delegates to defaultLogger’s Debugf method. Used for debugging // information, typically disabled in production. Thread-safe. // Example: // // ll.Level(lx.LevelDebug) // ll.Debugf("Debug %s", "mode") // Output: [] DEBUG: Debug mode func Debugf(format string, args ...any) { defaultLogger.Debugf(format, args...) } // Warn logs a message at Warn level with variadic arguments using the default logger. // It concatenates the arguments with spaces and delegates to defaultLogger’s Warn method. // Used for warning conditions that do not halt execution. Thread-safe. // Example: // // ll.Warn("Low", "memory") // Output: [] WARN: Low memory func Warn(args ...any) { defaultLogger.Warn(args...) } // Warnf logs a message at Warn level with a format string using the default logger. // It formats the message and delegates to defaultLogger’s Warnf method. Used for warning // conditions that do not halt execution. Thread-safe. // Example: // // ll.Warnf("Low %s", "memory") // Output: [] WARN: Low memory func Warnf(format string, args ...any) { defaultLogger.Warnf(format, args...) } // Error logs a message at Error level with variadic arguments using the default logger. // It concatenates the arguments with spaces and delegates to defaultLogger’s Error method. // Used for error conditions requiring attention. Thread-safe. // Example: // // ll.Error("Database", "failure") // Output: [] ERROR: Database failure func Error(args ...any) { defaultLogger.Error(args...) } // Errorf logs a message at Error level with a format string using the default logger. // It formats the message and delegates to defaultLogger’s Errorf method. Used for error // conditions requiring attention. Thread-safe. // Example: // // ll.Errorf("Database %s", "failure") // Output: [] ERROR: Database failure func Errorf(format string, args ...any) { defaultLogger.Errorf(format, args...) } // Stack logs a message at Error level with a stack trace and variadic arguments using the default logger. // It concatenates the arguments with spaces and delegates to defaultLogger’s Stack method. // Thread-safe. // Example: // // ll.Stack("Critical", "error") // Output: [] ERROR: Critical error [stack=...] func Stack(args ...any) { defaultLogger.Stack(args...) } // Stackf logs a message at Error level with a stack trace and a format string using the default logger. // It formats the message and delegates to defaultLogger’s Stackf method. Thread-safe. // Example: // // ll.Stackf("Critical %s", "error") // Output: [] ERROR: Critical error [stack=...] func Stackf(format string, args ...any) { defaultLogger.Stackf(format, args...) } // Fatal logs a message at Error level with a stack trace and variadic arguments using the default logger, // then exits. It concatenates the arguments with spaces, logs with a stack trace, and terminates // with exit code 1. Thread-safe. // Example: // // ll.Fatal("Fatal", "error") // Output: [] ERROR: Fatal error [stack=...], then exits func Fatal(args ...any) { defaultLogger.Fatal(args...) } // Fatalf logs a formatted message at Error level with a stack trace using the default logger, // then exits. It formats the message, logs with a stack trace, and terminates with exit code 1. // Thread-safe. // Example: // // ll.Fatalf("Fatal %s", "error") // Output: [] ERROR: Fatal error [stack=...], then exits func Fatalf(format string, args ...any) { defaultLogger.Fatalf(format, args...) } // Panic logs a message at Error level with a stack trace and variadic arguments using the default logger, // then panics. It concatenates the arguments with spaces, logs with a stack trace, and triggers a panic. // Thread-safe. // Example: // // ll.Panic("Panic", "error") // Output: [] ERROR: Panic error [stack=...], then panics func Panic(args ...any) { defaultLogger.Panic(args...) } // Panicf logs a formatted message at Error level with a stack trace using the default logger, // then panics. It formats the message, logs with a stack trace, and triggers a panic. Thread-safe. // Example: // // ll.Panicf("Panic %s", "error") // Output: [] ERROR: Panic error [stack=...], then panics func Panicf(format string, args ...any) { defaultLogger.Panicf(format, args...) } // If creates a conditional logger that logs only if the condition is true using the default logger. // It returns a Conditional struct that wraps the default logger, enabling conditional logging methods. // Thread-safe via the Logger’s mutex. // Example: // // ll.If(true).Info("Logged") // Output: [] INFO: Logged // ll.If(false).Info("Ignored") // No output func If(condition bool) *Conditional { return defaultLogger.If(condition) } // Context creates a new logger with additional contextual fields using the default logger. // It preserves existing context fields and adds new ones, returning a new logger instance // to avoid mutating the default logger. Thread-safe with write lock. // Example: // // logger := ll.Context(map[string]interface{}{"user": "alice"}) // logger.Info("Action") // Output: [] INFO: Action [user=alice] func Context(fields map[string]interface{}) *Logger { return defaultLogger.Context(fields) } // AddContext adds a key-value pair to the default logger’s context, modifying it directly. // It mutates the default logger’s context and is thread-safe using a write lock. // Example: // // ll.AddContext("user", "alice") // ll.Info("Action") // Output: [] INFO: Action [user=alice] func AddContext(key string, value interface{}) *Logger { return defaultLogger.AddContext(key, value) } // GetContext returns the default logger’s context map of persistent key-value fields. // It provides thread-safe read access to the context using a read lock. // Example: // // ll.AddContext("user", "alice") // ctx := ll.GetContext() // Returns map[string]interface{}{"user": "alice"} func GetContext() map[string]interface{} { return defaultLogger.GetContext() } // GetLevel returns the minimum log level for the default logger. // It provides thread-safe read access to the level field using a read lock. // Example: // // ll.Level(lx.LevelWarn) // if ll.GetLevel() == lx.LevelWarn { // ll.Warn("Warning level set") // Output: [] WARN: Warning level set // } func GetLevel() lx.LevelType { return defaultLogger.GetLevel() } // GetPath returns the default logger’s current namespace path. // It provides thread-safe read access to the currentPath field using a read lock. // Example: // // logger := ll.Namespace("app") // path := logger.GetPath() // Returns "app" func GetPath() string { return defaultLogger.GetPath() } // GetSeparator returns the default logger’s namespace separator (e.g., "/"). // It provides thread-safe read access to the separator field using a read lock. // Example: // // ll.Separator(".") // sep := ll.GetSeparator() // Returns "." func GetSeparator() string { return defaultLogger.GetSeparator() } // GetStyle returns the default logger’s namespace formatting style (FlatPath or NestedPath). // It provides thread-safe read access to the style field using a read lock. // Example: // // ll.Style(lx.NestedPath) // if ll.GetStyle() == lx.NestedPath { // ll.Info("Nested style") // Output: []: INFO: Nested style // } func GetStyle() lx.StyleType { return defaultLogger.GetStyle() } // GetHandler returns the default logger’s current handler for customization or inspection. // The returned handler should not be modified concurrently with logger operations. // Example: // // handler := ll.GetHandler() // Returns the current handler (e.g., TextHandler) func GetHandler() lx.Handler { return defaultLogger.GetHandler() } // Separator sets the namespace separator for the default logger (e.g., "/" or "."). // It updates the separator used in namespace paths. Thread-safe with write lock. // Returns the default logger for method chaining. // Example: // // ll.Separator(".") // ll.Namespace("app").Info("Log") // Output: [app] INFO: Log func Separator(separator string) *Logger { return defaultLogger.Separator(separator) } // Prefix sets a prefix to be prepended to all log messages of the default logger. // The prefix is applied before the message in the log output. Thread-safe with write lock. // Returns the default logger for method chaining. // Example: // // ll.Prefix("APP: ") // ll.Info("Started") // Output: [] INFO: APP: Started func Prefix(prefix string) *Logger { return defaultLogger.Prefix(prefix) } // StackSize sets the buffer size for stack trace capture in the default logger. // It configures the maximum size for stack traces in Stack, Fatal, and Panic methods. // Thread-safe with write lock. Returns the default logger for chaining. // Example: // // ll.StackSize(65536) // ll.Stack("Error") // Captures up to 64KB stack trace func StackSize(size int) *Logger { return defaultLogger.StackSize(size) } // Use adds a middleware function to process log entries before they are handled by the default logger. // It registers the middleware and returns a Middleware handle for removal. Middleware returning // a non-nil error stops the log. Thread-safe with write lock. // Example: // // mw := ll.Use(ll.FuncMiddleware(func(e *lx.Entry) error { // if e.Level < lx.LevelWarn { // return fmt.Errorf("level too low") // } // return nil // })) // ll.Info("Ignored") // No output // mw.Remove() // ll.Info("Logged") // Output: [] INFO: Logged func Use(fn lx.Handler) *Middleware { return defaultLogger.Use(fn) } // Remove removes middleware by the reference returned from Use for the default logger. // It delegates to the Middleware’s Remove method for thread-safe removal. // Example: // // mw := ll.Use(someMiddleware) // ll.Remove(mw) // Removes middleware func Remove(m *Middleware) { defaultLogger.Remove(m) } // Clear removes all middleware functions from the default logger. // It resets the middleware chain to empty, ensuring no middleware is applied. // Thread-safe with write lock. Returns the default logger for chaining. // Example: // // ll.Use(someMiddleware) // ll.Clear() // ll.Info("No middleware") // Output: [] INFO: No middleware func Clear() *Logger { return defaultLogger.Clear() } // CanLog checks if a log at the given level would be emitted by the default logger. // It considers enablement, log level, namespaces, sampling, and rate limits. // Thread-safe via the Logger’s shouldLog method. // Example: // // ll.Level(lx.LevelWarn) // canLog := ll.CanLog(lx.LevelInfo) // false func CanLog(level lx.LevelType) bool { return defaultLogger.CanLog(level) } // NamespaceEnabled checks if a namespace is enabled in the default logger. // It evaluates the namespace hierarchy, considering parent namespaces, and caches the result // for performance. Thread-safe with read lock. // Example: // // ll.NamespaceDisable("app/db") // enabled := ll.NamespaceEnabled("app/db") // false func NamespaceEnabled(path string) bool { return defaultLogger.NamespaceEnabled(path) } // Print logs a message at Info level without format specifiers using the default logger. // It concatenates variadic arguments with spaces, minimizing allocations, and delegates // to defaultLogger’s Print method. Thread-safe via the Logger’s log method. // Example: // // ll.Print("message", "value") // Output: [] INFO: message value func Print(args ...any) { defaultLogger.Print(args...) } // Println logs a message at Info level without format specifiers, minimizing allocations // by concatenating arguments with spaces. It is thread-safe via the log method. // Example: // // ll.Println("message", "value") // Output: [] INFO: message value [New Line] func Println(args ...any) { defaultLogger.Println(args...) } // Printf logs a message at Info level with a format string using the default logger. // It formats the message and delegates to defaultLogger’s Printf method. Thread-safe via // the Logger’s log method. // Example: // // ll.Printf("Message %s", "value") // Output: [] INFO: Message value func Printf(format string, args ...any) { defaultLogger.Printf(format, args...) } // Len returns the total number of log entries sent to the handler by the default logger. // It provides thread-safe access to the entries counter using atomic operations. // Example: // // ll.Info("Test") // count := ll.Len() // Returns 1 func Len() int64 { return defaultLogger.Len() } // Measure is a benchmarking helper that measures and returns the duration of a function’s execution. // It logs the duration at Info level with a "duration" field using defaultLogger. The function // is executed once, and the elapsed time is returned. Thread-safe via the Logger’s mutex. // Example: // // duration := ll.Measure(func() { time.Sleep(time.Millisecond) }) // // Output: [] INFO: function executed [duration=~1ms] func Measure(fns ...func()) time.Duration { start := time.Now() for _, fn := range fns { fn() } duration := time.Since(start) defaultLogger.Fields("duration", duration).Infof("function executed") return duration } // Benchmark logs the duration since a start time at Info level using the default logger. // It calculates the time elapsed since the provided start time and logs it with "start", // "end", and "duration" fields. Thread-safe via the Logger’s mutex. // Example: // // start := time.Now() // time.Sleep(time.Millisecond) // ll.Benchmark(start) // Output: [] INFO: benchmark [start=... end=... duration=...] func Benchmark(start time.Time) { defaultLogger.Fields("start", start, "end", time.Now(), "duration", time.Now().Sub(start)).Infof("benchmark") } // Clone returns a new logger with the same configuration as the default logger. // It creates a copy of defaultLogger’s settings (level, style, namespaces, etc.) but with // an independent context, allowing customization without affecting the global logger. // Thread-safe via the Logger’s Clone method. // Example: // // logger := ll.Clone().Namespace("sub") // logger.Info("Sub-logger") // Output: [sub] INFO: Sub-logger func Clone() *Logger { return defaultLogger.Clone() } // Err adds one or more errors to the default logger’s context and logs them. // It stores non-nil errors in the "error" context field and logs their concatenated string // representations (e.g., "failed 1; failed 2") at the Error level. Thread-safe via the Logger’s mutex. // Example: // // err1 := errors.New("failed 1") // ll.Err(err1) // ll.Info("Error occurred") // Output: [] ERROR: failed 1 // // [] INFO: Error occurred [error=failed 1] func Err(errs ...error) { defaultLogger.Err(errs...) } // Start activates the global logging system. // If the system was shut down, this re-enables all logging operations, // subject to individual logger and namespace configurations. // Thread-safe via atomic operations. // Example: // // ll.Shutdown() // ll.Info("Ignored") // No output // ll.Start() // ll.Info("Logged") // Output: [] INFO: Logged func Start() { atomic.StoreInt32(&systemActive, 1) } // Shutdown deactivates the global logging system. // All logging operations are skipped, regardless of individual logger or namespace configurations, // until Start() is called again. Thread-safe via atomic operations. // Example: // // ll.Shutdown() // ll.Info("Ignored") // No output func Shutdown() { atomic.StoreInt32(&systemActive, 0) } // Active returns true if the global logging system is currently active. // Thread-safe via atomic operations. // Example: // // if ll.Active() { // ll.Info("System active") // Output: [] INFO: System active // } func Active() bool { return atomic.LoadInt32(&systemActive) == 1 } // Enable activates logging for the default logger. // It allows logs to be emitted if other conditions (level, namespace) are met. // Thread-safe with write lock. Returns the default logger for method chaining. // Example: // // ll.Disable() // ll.Info("Ignored") // No output // ll.Enable() // ll.Info("Logged") // Output: [] INFO: Logged func Enable() *Logger { return defaultLogger.Enable() } // Disable deactivates logging for the default logger. // It suppresses all logs, regardless of level or namespace. Thread-safe with write lock. // Returns the default logger for method chaining. // Example: // // ll.Disable() // ll.Info("Ignored") // No output func Disable() *Logger { return defaultLogger.Disable() } // Dbg logs debug information including the source file, line number, and expression value // using the default logger. It captures the calling line of code and displays both the // expression and its value. Useful for debugging without temporary print statements. // Example: // // x := 42 // ll.Dbg(x) // Output: [file.go:123] x = 42 func Dbg(any ...interface{}) { defaultLogger.dbg(2, any...) } // Dump displays a hex and ASCII representation of a value’s binary form using the default logger. // It serializes the value using gob encoding or direct conversion and shows a hex/ASCII dump. // Useful for inspecting binary data structures. // Example: // // ll.Dump([]byte{0x41, 0x42}) // Outputs hex/ASCII dump func Dump(any interface{}) { defaultLogger.Dump(any) } // Enabled returns whether the default logger is enabled for logging. // It provides thread-safe read access to the enabled field using a read lock. // Example: // // ll.Enable() // if ll.Enabled() { // ll.Info("Logging enabled") // Output: [] INFO: Logging enabled // } func Enabled() bool { return defaultLogger.Enabled() } // Fields starts a fluent chain for adding fields using variadic key-value pairs with the default logger. // It creates a FieldBuilder to attach fields, handling non-string keys or uneven pairs by // adding an error field. Thread-safe via the FieldBuilder’s logger. // Example: // // ll.Fields("user", "alice").Info("Action") // Output: [] INFO: Action [user=alice] func Fields(pairs ...any) *FieldBuilder { return defaultLogger.Fields(pairs...) } // Field starts a fluent chain for adding fields from a map with the default logger. // It creates a FieldBuilder to attach fields from a map, supporting type-safe field addition. // Thread-safe via the FieldBuilder’s logger. // Example: // // ll.Field(map[string]interface{}{"user": "alice"}).Info("Action") // Output: [] INFO: Action [user=alice] func Field(fields map[string]interface{}) *FieldBuilder { return defaultLogger.Field(fields) } // Line adds vertical spacing (newlines) to the log output using the default logger. // If no arguments are provided, it defaults to 1 newline. Multiple values are summed to // determine the total lines. Useful for visually separating log sections. Thread-safe. // Example: // // ll.Line(2).Info("After two newlines") // Adds 2 blank lines before: [] INFO: After two newlines func Line(lines ...int) *Logger { return defaultLogger.Line(lines...) } // Indent sets the indentation level for all log messages of the default logger. // Each level adds two spaces to the log message, useful for hierarchical output. // Thread-safe with write lock. Returns the default logger for method chaining. // Example: // // ll.Indent(2) // ll.Info("Indented") // Output: [] INFO: Indented func Indent(depth int) *Logger { return defaultLogger.Indent(depth) } // Mark logs the current file and line number where it's called, without any additional debug information. // It's useful for tracing execution flow without the verbosity of Dbg. // Example: // // logger.Mark() // *MARK*: [file.go:123] func Mark(names ...string) { defaultLogger.mark(2, names...) } golang-github-olekukonko-ll-0.0.9/go.mod000066400000000000000000000000511504320775200201420ustar00rootroot00000000000000module github.com/olekukonko/ll go 1.21 golang-github-olekukonko-ll-0.0.9/lc.go000066400000000000000000000036421504320775200177720ustar00rootroot00000000000000package ll import "github.com/olekukonko/ll/lx" // defaultStore is the global namespace store for enable/disable states. // It is shared across all Logger instances to manage namespace hierarchy consistently. // Thread-safe via the lx.Namespace struct’s sync.Map. var defaultStore = &lx.Namespace{} // systemActive indicates if the global logging system is active. // Defaults to true, meaning logging is active unless explicitly shut down. // Or, default to false and require an explicit ll.Start(). Let's default to true for less surprise. var systemActive int32 = 1 // 1 for true, 0 for false (for atomic operations) // Option defines a functional option for configuring a Logger. type Option func(*Logger) // reverseString reverses the input string by swapping characters from both ends. // It converts the string to a rune slice to handle Unicode characters correctly, // ensuring proper reversal for multi-byte characters. // Used internally for string manipulation, such as in debugging or log formatting. func reverseString(s string) string { // Convert string to rune slice to handle Unicode characters r := []rune(s) // Iterate over half the slice, swapping characters from start and end for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } // Convert rune slice back to string and return return string(r) } // viewString converts a byte slice to a printable string, replacing non-printable // characters (ASCII < 32 or > 126) with a dot ('.'). // It ensures safe display of binary data in logs, such as in the Dump method. // Used for formatting binary data in a human-readable hex/ASCII dump. func viewString(b []byte) string { // Convert byte slice to rune slice via string for processing r := []rune(string(b)) // Replace non-printable characters with '.' for i := range r { if r[i] < 32 || r[i] > 126 { r[i] = '.' } } // Return the resulting printable string return string(r) } golang-github-olekukonko-ll-0.0.9/lh/000077500000000000000000000000001504320775200174435ustar00rootroot00000000000000golang-github-olekukonko-ll-0.0.9/lh/buffered.go000066400000000000000000000232461504320775200215630ustar00rootroot00000000000000package lh import ( "fmt" "io" "os" "runtime" "sync" "time" "github.com/olekukonko/ll/lx" ) // Buffering holds configuration for the Buffered handler. type Buffering struct { BatchSize int // Flush when this many entries are buffered (default: 100) FlushInterval time.Duration // Maximum time between flushes (default: 10s) MaxBuffer int // Maximum buffer size before applying backpressure (default: 1000) OnOverflow func(int) // Called when buffer reaches MaxBuffer (default: logs warning) } // BufferingOpt configures Buffered handler. type BufferingOpt func(*Buffering) // WithBatchSize sets the batch size for flushing. // It specifies the number of log entries to buffer before flushing to the underlying handler. // Example: // // handler := NewBuffered(textHandler, WithBatchSize(50)) // Flush every 50 entries func WithBatchSize(size int) BufferingOpt { return func(c *Buffering) { c.BatchSize = size } } // WithFlushInterval sets the maximum time between flushes. // It defines the interval at which buffered entries are flushed, even if the batch size is not reached. // Example: // // handler := NewBuffered(textHandler, WithFlushInterval(5*time.Second)) // Flush every 5 seconds func WithFlushInterval(d time.Duration) BufferingOpt { return func(c *Buffering) { c.FlushInterval = d } } // WithMaxBuffer sets the maximum buffer size before backpressure. // It limits the number of entries that can be queued in the channel, triggering overflow handling if exceeded. // Example: // // handler := NewBuffered(textHandler, WithMaxBuffer(500)) // Allow up to 500 buffered entries func WithMaxBuffer(size int) BufferingOpt { return func(c *Buffering) { c.MaxBuffer = size } } // WithOverflowHandler sets the overflow callback. // It specifies a function to call when the buffer reaches MaxBuffer, typically for logging or metrics. // Example: // // handler := NewBuffered(textHandler, WithOverflowHandler(func(n int) { fmt.Printf("Overflow: %d entries\n", n) })) func WithOverflowHandler(fn func(int)) BufferingOpt { return func(c *Buffering) { c.OnOverflow = fn } } // Buffered wraps any Handler to provide buffering capabilities. // It buffers log entries in a channel and flushes them based on batch size, time interval, or explicit flush. // The generic type H ensures compatibility with any lx.Handler implementation. // Thread-safe via channels and sync primitives. type Buffered[H lx.Handler] struct { handler H // Underlying handler to process log entries config *Buffering // Configuration for batching and flushing entries chan *lx.Entry // Channel for buffering log entries flushSignal chan struct{} // Channel to trigger explicit flushes shutdown chan struct{} // Channel to signal worker shutdown shutdownOnce sync.Once // Ensures Close is called only once wg sync.WaitGroup // Waits for worker goroutine to finish } // NewBuffered creates a new buffered handler that wraps another handler. // It initializes the handler with default or provided configuration options and starts a worker goroutine. // Thread-safe via channel operations and finalizer for cleanup. // Example: // // textHandler := lh.NewTextHandler(os.Stdout) // buffered := NewBuffered(textHandler, WithBatchSize(50)) func NewBuffered[H lx.Handler](handler H, opts ...BufferingOpt) *Buffered[H] { // Initialize default configuration config := &Buffering{ BatchSize: 100, // Default: flush every 100 entries FlushInterval: 10 * time.Second, // Default: flush every 10 seconds MaxBuffer: 1000, // Default: max 1000 entries in buffer OnOverflow: func(count int) { // Default: log overflow to io.Discard fmt.Fprintf(io.Discard, "log buffer overflow: %d entries\n", count) }, } // Apply provided options for _, opt := range opts { opt(config) } // Ensure sane configuration values if config.BatchSize < 1 { config.BatchSize = 1 // Minimum batch size is 1 } if config.MaxBuffer < config.BatchSize { config.MaxBuffer = config.BatchSize * 10 // Ensure buffer is at least 10x batch size } if config.FlushInterval <= 0 { config.FlushInterval = 10 * time.Second // Minimum flush interval is 10s } // Initialize Buffered handler b := &Buffered[H]{ handler: handler, // Set underlying handler config: config, // Set configuration entries: make(chan *lx.Entry, config.MaxBuffer), // Create buffered channel flushSignal: make(chan struct{}, 1), // Create single-slot flush signal channel shutdown: make(chan struct{}), // Create shutdown signal channel } // Start worker goroutine b.wg.Add(1) go b.worker() // Set finalizer for cleanup during garbage collection runtime.SetFinalizer(b, (*Buffered[H]).Final) return b } // Handle implements the lx.Handler interface. // It buffers log entries in the entries channel or triggers a flush on overflow. // Returns an error if the buffer is full and flush cannot be triggered. // Thread-safe via non-blocking channel operations. // Example: // // buffered.Handle(&lx.Entry{Message: "test"}) // Buffers entry or triggers flush func (b *Buffered[H]) Handle(e *lx.Entry) error { select { case b.entries <- e: // Buffer entry if channel has space return nil default: // Handle buffer overflow if b.config.OnOverflow != nil { b.config.OnOverflow(len(b.entries)) // Call overflow handler } select { case b.flushSignal <- struct{}{}: // Trigger flush if possible return fmt.Errorf("log buffer overflow, triggering flush") default: // Flush already in progress return fmt.Errorf("log buffer overflow and flush already in progress") } } } // Flush triggers an immediate flush of buffered entries. // It sends a signal to the worker to process all buffered entries. // If a flush is already pending, it waits briefly and may exit without flushing. // Thread-safe via non-blocking channel operations. // Example: // // buffered.Flush() // Flushes all buffered entries func (b *Buffered[H]) Flush() { select { case b.flushSignal <- struct{}{}: // Signal worker to flush case <-time.After(100 * time.Millisecond): // Timeout if flush is pending // Flush already pending } } // Close flushes any remaining entries and stops the worker. // It ensures shutdown is performed only once and waits for the worker to finish. // Thread-safe via sync.Once and WaitGroup. // Returns nil as it does not produce errors. // Example: // // buffered.Close() // Flushes entries and stops worker func (b *Buffered[H]) Close() error { b.shutdownOnce.Do(func() { close(b.shutdown) // Signal worker to shut down b.wg.Wait() // Wait for worker to finish runtime.SetFinalizer(b, nil) // Remove finalizer }) return nil } // Final ensures remaining entries are flushed during garbage collection. // It calls Close to flush entries and stop the worker. // Used as a runtime finalizer to prevent log loss. // Example (internal usage): // // runtime.SetFinalizer(buffered, (*Buffered[H]).Final) func (b *Buffered[H]) Final() { b.Close() } // Config returns the current configuration of the Buffered handler. // It provides access to BatchSize, FlushInterval, MaxBuffer, and OnOverflow settings. // Example: // // config := buffered.Config() // Access configuration func (b *Buffered[H]) Config() *Buffering { return b.config } // worker processes entries and handles flushing. // It runs in a goroutine, buffering entries, flushing on batch size, timer, or explicit signal, // and shutting down cleanly when signaled. // Thread-safe via channel operations and WaitGroup. func (b *Buffered[H]) worker() { defer b.wg.Done() // Signal completion when worker exits batch := make([]*lx.Entry, 0, b.config.BatchSize) // Buffer for batching entries ticker := time.NewTicker(b.config.FlushInterval) // Timer for periodic flushes defer ticker.Stop() // Clean up ticker for { select { case entry := <-b.entries: // Receive new entry batch = append(batch, entry) // Flush if batch size is reached if len(batch) >= b.config.BatchSize { b.flushBatch(batch) batch = batch[:0] } case <-ticker.C: // Periodic flush if len(batch) > 0 { b.flushBatch(batch) batch = batch[:0] } case <-b.flushSignal: // Explicit flush if len(batch) > 0 { b.flushBatch(batch) batch = batch[:0] } b.drainRemaining() // Drain all entries from the channel case <-b.shutdown: // Shutdown signal if len(batch) > 0 { b.flushBatch(batch) } b.drainRemaining() // Flush remaining entries return } } } // flushBatch processes a batch of entries through the wrapped handler. // It writes each entry to the underlying handler, logging any errors to stderr. // Example (internal usage): // // b.flushBatch([]*lx.Entry{entry1, entry2}) func (b *Buffered[H]) flushBatch(batch []*lx.Entry) { for _, entry := range batch { // Process each entry through the handler if err := b.handler.Handle(entry); err != nil { fmt.Fprintf(os.Stderr, "log flush error: %v\n", err) // Log errors to stderr } } } // drainRemaining processes any remaining entries in the channel. // It flushes all entries from the entries channel to the underlying handler, // logging any errors to stderr. Used during flush or shutdown. // Example (internal usage): // // b.drainRemaining() // Flushes all pending entries func (b *Buffered[H]) drainRemaining() { for { select { case entry := <-b.entries: // Process next entry if err := b.handler.Handle(entry); err != nil { fmt.Fprintf(os.Stderr, "log drain error: %v\n", err) // Log errors to stderr } default: // Exit when channel is empty return } } } golang-github-olekukonko-ll-0.0.9/lh/colorized.go000066400000000000000000000361111504320775200217660ustar00rootroot00000000000000package lh import ( "fmt" "github.com/olekukonko/ll/lx" "io" "os" "sort" "strings" "time" ) // Palette defines ANSI color codes for various log components. // It specifies colors for headers, goroutines, functions, paths, stack traces, and log levels, // used by ColorizedHandler to format log output with color. type Palette struct { Header string // Color for stack trace header and dump separators Goroutine string // Color for goroutine lines in stack traces Func string // Color for function names in stack traces Path string // Color for file paths in stack traces FileLine string // Color for file line numbers (not used in provided code) Reset string // Reset code to clear color formatting Pos string // Color for position in hex dumps Hex string // Color for hex values in dumps Ascii string // Color for ASCII values in dumps Debug string // Color for Debug level messages Info string // Color for Info level messages Warn string // Color for Warn level messages Error string // Color for Error level messages Title string // Color for dump titles (BEGIN/END separators) } // darkPalette defines colors optimized for dark terminal backgrounds. // It uses bright, contrasting colors for readability on dark backgrounds. var darkPalette = Palette{ Header: "\033[1;31m", // Bold red for headers Goroutine: "\033[1;36m", // Bold cyan for goroutines Func: "\033[97m", // Bright white for functions Path: "\033[38;5;245m", // Light gray for paths FileLine: "\033[38;5;111m", // Muted light blue (unused) Reset: "\033[0m", // Reset color formatting Title: "\033[38;5;245m", // Light gray for dump titles Pos: "\033[38;5;117m", // Light blue for dump positions Hex: "\033[38;5;156m", // Light green for hex values Ascii: "\033[38;5;224m", // Light pink for ASCII values Debug: "\033[36m", // Cyan for Debug level Info: "\033[32m", // Green for Info level Warn: "\033[33m", // Yellow for Warn level Error: "\033[31m", // Red for Error level } // lightPalette defines colors optimized for light terminal backgrounds. // It uses darker colors for better contrast on light backgrounds. var lightPalette = Palette{ Header: "\033[1;31m", // Same red for headers Goroutine: "\033[34m", // Blue (darker for light bg) Func: "\033[30m", // Black text for functions Path: "\033[90m", // Dark gray for paths FileLine: "\033[94m", // Blue for file lines (unused) Reset: "\033[0m", // Reset color formatting Title: "\033[38;5;245m", // Light gray for dump titles Pos: "\033[38;5;117m", // Light blue for dump positions Hex: "\033[38;5;156m", // Light green for hex values Ascii: "\033[38;5;224m", // Light pink for ASCII values Debug: "\033[36m", // Cyan for Debug level Info: "\033[32m", // Green for Info level Warn: "\033[33m", // Yellow for Warn level Error: "\033[31m", // Red for Error level } // ColorizedHandler is a handler that outputs log entries with ANSI color codes. // It formats log entries with colored namespace, level, message, fields, and stack traces, // writing the result to the provided writer. // Thread-safe if the underlying writer is thread-safe. type ColorizedHandler struct { w io.Writer // Destination for colored log output palette Palette // Color scheme for formatting showTime bool // Whether to display timestamps timeFormat string // Format for timestamps (defaults to time.RFC3339) } // ColorOption defines a configuration function for ColorizedHandler. // It allows customization of the handler, such as setting the color palette. type ColorOption func(*ColorizedHandler) // WithColorPallet sets the color palette for the ColorizedHandler. // It allows specifying a custom Palette for dark or light terminal backgrounds. // Example: // // handler := NewColorizedHandler(os.Stdout, WithColorPallet(lightPalette)) func WithColorPallet(pallet Palette) ColorOption { return func(c *ColorizedHandler) { c.palette = pallet } } // NewColorizedHandler creates a new ColorizedHandler writing to the specified writer. // It initializes the handler with a detected or specified color palette and applies // optional configuration functions. // Example: // // handler := NewColorizedHandler(os.Stdout) // logger := ll.New("app").Enable().Handler(handler) // logger.Info("Test") // Output: [app] : Test func NewColorizedHandler(w io.Writer, opts ...ColorOption) *ColorizedHandler { // Initialize with writer c := &ColorizedHandler{w: w, showTime: false, timeFormat: time.RFC3339, } // Apply configuration options for _, opt := range opts { opt(c) } // Detect palette if not set c.palette = c.detectPalette() return c } // Handle processes a log entry and writes it with ANSI color codes. // It delegates to specialized methods based on the entry's class (Dump, Raw, or regular). // Returns an error if writing to the underlying writer fails. // Thread-safe if the writer is thread-safe. // Example: // // handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes colored output func (h *ColorizedHandler) Handle(e *lx.Entry) error { switch e.Class { case lx.ClassDump: // Handle hex dump entries return h.handleDumpOutput(e) case lx.ClassRaw: // Write raw entries directly _, err := h.w.Write([]byte(e.Message)) return err default: // Handle standard log entries return h.handleRegularOutput(e) } } // Timestamped enables or disables timestamp display and optionally sets a custom time format. // If format is empty, defaults to RFC3339. // Example: // // handler := NewColorizedHandler(os.Stdout).Timestamped(true, time.StampMilli) // // Output: Jan 02 15:04:05.000 [app] INFO: Test func (h *ColorizedHandler) Timestamped(enable bool, format ...string) { h.showTime = enable if len(format) > 0 && format[0] != "" { h.timeFormat = format[0] } } // handleRegularOutput handles normal log entries. // It formats the entry with colored namespace, level, message, fields, and stack trace (if present), // writing the result to the handler's writer. // Returns an error if writing fails. // Example (internal usage): // // h.handleRegularOutput(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes colored output func (h *ColorizedHandler) handleRegularOutput(e *lx.Entry) error { var builder strings.Builder // Buffer for building formatted output // Add timestamp if enabled if h.showTime { builder.WriteString(e.Timestamp.Format(h.timeFormat)) builder.WriteString(lx.Space) } // Format namespace with colors h.formatNamespace(&builder, e) // Format level with color based on severity h.formatLevel(&builder, e) // Add message and fields builder.WriteString(e.Message) h.formatFields(&builder, e) // fmt.Println("------------>", len(e.Stack)) // Format stack trace if present if len(e.Stack) > 0 { h.formatStack(&builder, e.Stack) } // Append newline for non-None levels if e.Level != lx.LevelNone { builder.WriteString(lx.Newline) } // Write formatted output to writer _, err := h.w.Write([]byte(builder.String())) return err } // formatNamespace formats the namespace with ANSI color codes. // It supports FlatPath ([parent/child]) and NestedPath ([parent]→[child]) styles. // Example (internal usage): // // h.formatNamespace(&builder, &lx.Entry{Namespace: "parent/child", Style: lx.FlatPath}) // Writes "[parent/child]: " func (h *ColorizedHandler) formatNamespace(b *strings.Builder, e *lx.Entry) { if e.Namespace == "" { return } b.WriteString(lx.LeftBracket) switch e.Style { case lx.NestedPath: // Split namespace and format as [parent]→[child] parts := strings.Split(e.Namespace, lx.Slash) for i, part := range parts { b.WriteString(part) b.WriteString(lx.RightBracket) if i < len(parts)-1 { b.WriteString(lx.Arrow) b.WriteString(lx.LeftBracket) } } default: // FlatPath // Format as [parent/child] b.WriteString(e.Namespace) b.WriteString(lx.RightBracket) } b.WriteString(lx.Colon) b.WriteString(lx.Space) } // formatLevel formats the log level with ANSI color codes. // It applies a color based on the level (Debug, Info, Warn, Error) and resets afterward. // Example (internal usage): // // h.formatLevel(&builder, &lx.Entry{Level: lx.LevelInfo}) // Writes "INFO: " func (h *ColorizedHandler) formatLevel(b *strings.Builder, e *lx.Entry) { // Map levels to colors color := map[lx.LevelType]string{ lx.LevelDebug: h.palette.Debug, // Cyan lx.LevelInfo: h.palette.Info, // Green lx.LevelWarn: h.palette.Warn, // Yellow lx.LevelError: h.palette.Error, // Red }[e.Level] b.WriteString(color) b.WriteString(e.Level.String()) b.WriteString(h.palette.Reset) b.WriteString(lx.Colon) b.WriteString(lx.Space) } // formatFields formats the log entry's fields in sorted order. // It writes fields as [key=value key=value], with no additional coloring. // Example (internal usage): // // h.formatFields(&builder, &lx.Entry{Fields: map[string]interface{}{"key": "value"}}) // Writes " [key=value]" func (h *ColorizedHandler) formatFields(b *strings.Builder, e *lx.Entry) { if len(e.Fields) == 0 { return } // Collect and sort field keys var keys []string for k := range e.Fields { keys = append(keys, k) } sort.Strings(keys) b.WriteString(lx.Space) b.WriteString(lx.LeftBracket) // Format fields as key=value for i, k := range keys { if i > 0 { b.WriteString(lx.Space) } b.WriteString(k) b.WriteString("=") b.WriteString(fmt.Sprint(e.Fields[k])) } b.WriteString(lx.RightBracket) } // formatStack formats a stack trace with ANSI color codes. // It structures the stack trace with colored goroutine, function, and path segments, // using indentation and separators for readability. // Example (internal usage): // // h.formatStack(&builder, []byte("goroutine 1 [running]:\nmain.main()\n\tmain.go:10")) // Appends colored stack trace func (h *ColorizedHandler) formatStack(b *strings.Builder, stack []byte) { b.WriteString("\n") b.WriteString(h.palette.Header) b.WriteString("[stack]") b.WriteString(h.palette.Reset) b.WriteString("\n") lines := strings.Split(string(stack), "\n") if len(lines) == 0 { return } // Format goroutine line b.WriteString(" ┌─ ") b.WriteString(h.palette.Goroutine) b.WriteString(lines[0]) b.WriteString(h.palette.Reset) b.WriteString("\n") // Pair function name and file path lines for i := 1; i < len(lines)-1; i += 2 { funcLine := strings.TrimSpace(lines[i]) pathLine := strings.TrimSpace(lines[i+1]) if funcLine != "" { b.WriteString(" │ ") b.WriteString(h.palette.Func) b.WriteString(funcLine) b.WriteString(h.palette.Reset) b.WriteString("\n") } if pathLine != "" { b.WriteString(" │ ") // Look for last "/" before ".go:" lastSlash := strings.LastIndex(pathLine, "/") goIndex := strings.Index(pathLine, ".go:") if lastSlash >= 0 && goIndex > lastSlash { // Prefix path prefix := pathLine[:lastSlash+1] // File and line (e.g., ll.go:698 +0x5c) suffix := pathLine[lastSlash+1:] b.WriteString(h.palette.Path) b.WriteString(prefix) b.WriteString(h.palette.Reset) b.WriteString(h.palette.Path) // Use mainPath color for suffix b.WriteString(suffix) b.WriteString(h.palette.Reset) } else { // Fallback: whole line is gray b.WriteString(h.palette.Path) b.WriteString(pathLine) b.WriteString(h.palette.Reset) } b.WriteString("\n") } } // Handle any remaining unpaired line if len(lines)%2 == 0 && strings.TrimSpace(lines[len(lines)-1]) != "" { b.WriteString(" │ ") b.WriteString(h.palette.Func) b.WriteString(strings.TrimSpace(lines[len(lines)-1])) b.WriteString(h.palette.Reset) b.WriteString("\n") } b.WriteString(" └\n") } // handleDumpOutput formats hex dump output with ANSI color codes. // It applies colors to position, hex, ASCII, and title components of the dump, // wrapping the output with colored BEGIN/END separators. // Returns an error if writing fails. // Example (internal usage): // // h.handleDumpOutput(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61 62 'ab'"}) // Writes colored dump func (h *ColorizedHandler) handleDumpOutput(e *lx.Entry) error { var builder strings.Builder // Add timestamp if enabled if h.showTime { builder.WriteString(e.Timestamp.Format(h.timeFormat)) builder.WriteString(lx.Newline) } // Write colored BEGIN separator builder.WriteString(h.palette.Title) builder.WriteString("---- BEGIN DUMP ----") builder.WriteString(h.palette.Reset) builder.WriteString("\n") // Process each line of the dump lines := strings.Split(e.Message, "\n") length := len(lines) for i, line := range lines { if strings.HasPrefix(line, "pos ") { // Parse and color position and hex/ASCII parts parts := strings.SplitN(line, "hex:", 2) if len(parts) == 2 { builder.WriteString(h.palette.Pos) builder.WriteString(parts[0]) builder.WriteString(h.palette.Reset) hexAscii := strings.SplitN(parts[1], "'", 2) builder.WriteString(h.palette.Hex) builder.WriteString("hex:") builder.WriteString(hexAscii[0]) builder.WriteString(h.palette.Reset) if len(hexAscii) > 1 { builder.WriteString(h.palette.Ascii) builder.WriteString("'") builder.WriteString(hexAscii[1]) builder.WriteString(h.palette.Reset) } } } else if strings.HasPrefix(line, "Dumping value of type:") { // Color type dump lines builder.WriteString(h.palette.Header) builder.WriteString(line) builder.WriteString(h.palette.Reset) } else { // Write non-dump lines as-is builder.WriteString(line) } // Don't add newline for the last line if i < length-1 { builder.WriteString("\n") } } // Write colored END separator builder.WriteString(h.palette.Title) builder.WriteString("---- END DUMP ----") builder.WriteString(h.palette.Reset) builder.WriteString("\n") // Write formatted output to writer _, err := h.w.Write([]byte(builder.String())) return err } // detectPalette selects a color palette based on terminal environment variables. // It checks TERM_BACKGROUND, COLORFGBG, and AppleInterfaceStyle to determine // whether a light or dark palette is appropriate, defaulting to darkPalette. // Example (internal usage): // // palette := h.detectPalette() // Returns darkPalette or lightPalette func (h *ColorizedHandler) detectPalette() Palette { // Check TERM_BACKGROUND (e.g., iTerm2) if bg, ok := os.LookupEnv("TERM_BACKGROUND"); ok { if bg == "light" { return lightPalette // Use light palette for light background } return darkPalette // Use dark palette otherwise } // Check COLORFGBG (traditional xterm) if fgBg, ok := os.LookupEnv("COLORFGBG"); ok { parts := strings.Split(fgBg, ";") if len(parts) >= 2 { bg := parts[len(parts)-1] // Last part (some terminals add more fields) if bg == "7" || bg == "15" || bg == "0;15" { // Handle variations return lightPalette // Use light palette for light background } } } // Check macOS dark mode if style, ok := os.LookupEnv("AppleInterfaceStyle"); ok && strings.EqualFold(style, "dark") { return darkPalette // Use dark palette for macOS dark mode } // Default: dark (conservative choice for terminals) return darkPalette } golang-github-olekukonko-ll-0.0.9/lh/json.go000066400000000000000000000152261504320775200207510ustar00rootroot00000000000000package lh import ( "encoding/json" "fmt" "github.com/olekukonko/ll/lx" "io" "os" "strings" "sync" "time" ) // JSONHandler is a handler that outputs log entries as JSON objects. // It formats log entries with timestamp, level, message, namespace, fields, and optional // stack traces or dump segments, writing the result to the provided writer. // Thread-safe with a mutex to protect concurrent writes. type JSONHandler struct { writer io.Writer // Destination for JSON output timeFmt string // Format for timestamp (default: RFC3339Nano) pretty bool // Enable pretty printing with indentation if true fieldMap map[string]string // Optional mapping for field names (not used in provided code) mu sync.Mutex // Protects concurrent access to writer } // JsonOutput represents the JSON structure for a log entry. // It includes all relevant log data, such as timestamp, level, message, and optional // stack trace or dump segments, serialized as a JSON object. type JsonOutput struct { Time string `json:"ts"` // Timestamp in specified format Level string `json:"lvl"` // Log level (e.g., "INFO") Class string `json:"class"` // Entry class (e.g., "Text", "Dump") Msg string `json:"msg"` // Log message Namespace string `json:"ns"` // Namespace path Stack []byte `json:"stack"` // Stack trace (if present) Dump []dumpSegment `json:"dump"` // Hex/ASCII dump segments (for ClassDump) Fields map[string]interface{} `json:"fields"` // Custom fields } // dumpSegment represents a single segment of a hex/ASCII dump. // Used for ClassDump entries to structure position, hex values, and ASCII representation. type dumpSegment struct { Offset int `json:"offset"` // Starting byte offset of the segment Hex []string `json:"hex"` // Hexadecimal values of bytes ASCII string `json:"ascii"` // ASCII representation of bytes } // NewJSONHandler creates a new JSONHandler writing to the specified writer. // It initializes the handler with a default timestamp format (RFC3339Nano) and optional // configuration functions to customize settings like pretty printing. // Example: // // handler := NewJSONHandler(os.Stdout) // logger := ll.New("app").Enable().Handler(handler) // logger.Info("Test") // Output: {"ts":"...","lvl":"INFO","class":"Text","msg":"Test","ns":"app","stack":null,"dump":null,"fields":null} func NewJSONHandler(w io.Writer, opts ...func(*JSONHandler)) *JSONHandler { h := &JSONHandler{ writer: w, // Set output writer timeFmt: time.RFC3339Nano, // Default timestamp format } // Apply configuration options for _, opt := range opts { opt(h) } return h } // Handle processes a log entry and writes it as JSON. // It delegates to specialized methods based on the entry's class (Dump or regular), // ensuring thread-safety with a mutex. // Returns an error if JSON encoding or writing fails. // Example: // // handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes JSON object func (h *JSONHandler) Handle(e *lx.Entry) error { h.mu.Lock() defer h.mu.Unlock() // Handle dump entries separately if e.Class == lx.ClassDump { return h.handleDump(e) } // Handle standard log entries return h.handleRegular(e) } // handleRegular handles standard log entries (non-dump). // It converts the entry to a JsonOutput struct and encodes it as JSON, // applying pretty printing if enabled. Logs encoding errors to stderr for debugging. // Returns an error if encoding or writing fails. // Example (internal usage): // // h.handleRegular(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes JSON object func (h *JSONHandler) handleRegular(e *lx.Entry) error { // Create JSON output structure entry := JsonOutput{ Time: e.Timestamp.Format(h.timeFmt), // Format timestamp Level: e.Level.String(), // Convert level to string Class: e.Class.String(), // Convert class to string Msg: e.Message, // Set message Namespace: e.Namespace, // Set namespace Dump: nil, // No dump for regular entries Fields: e.Fields, // Copy fields Stack: e.Stack, // Include stack trace if present } // Create JSON encoder enc := json.NewEncoder(h.writer) if h.pretty { // Enable indentation for pretty printing enc.SetIndent("", " ") } // Log encoding attempt for debugging fmt.Fprintf(os.Stderr, "Encoding JSON entry: %v\n", e.Message) // Encode and write JSON err := enc.Encode(entry) if err != nil { // Log encoding error for debugging fmt.Fprintf(os.Stderr, "JSON encode error: %v\n", err) } return err } // handleDump processes ClassDump entries, converting hex dump output to JSON segments. // It parses the dump message into structured segments with offset, hex, and ASCII data, // encoding them as a JsonOutput struct. // Returns an error if parsing or encoding fails. // Example (internal usage): // // h.handleDump(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61 62 'ab'"}) // Writes JSON with dump segments func (h *JSONHandler) handleDump(e *lx.Entry) error { var segments []dumpSegment lines := strings.Split(e.Message, "\n") // Parse each line of the dump message for _, line := range lines { if !strings.HasPrefix(line, "pos") { continue // Skip non-dump lines } parts := strings.SplitN(line, "hex:", 2) if len(parts) != 2 { continue // Skip invalid lines } // Parse position var offset int fmt.Sscanf(parts[0], "pos %d", &offset) // Parse hex and ASCII hexAscii := strings.SplitN(parts[1], "'", 2) hexStr := strings.Fields(strings.TrimSpace(hexAscii[0])) // Create dump segment segments = append(segments, dumpSegment{ Offset: offset, // Set byte offset Hex: hexStr, // Set hex values ASCII: strings.Trim(hexAscii[1], "'"), // Set ASCII representation }) } // Encode JSON output with dump segments return json.NewEncoder(h.writer).Encode(JsonOutput{ Time: e.Timestamp.Format(h.timeFmt), // Format timestamp Level: e.Level.String(), // Convert level to string Class: e.Class.String(), // Convert class to string Msg: "dumping segments", // Fixed message for dumps Namespace: e.Namespace, // Set namespace Dump: segments, // Include parsed segments Fields: e.Fields, // Copy fields Stack: e.Stack, // Include stack trace if present }) } golang-github-olekukonko-ll-0.0.9/lh/memory.go000066400000000000000000000074601504320775200213110ustar00rootroot00000000000000package lh import ( "fmt" "github.com/olekukonko/ll/lx" "io" "sync" ) // MemoryHandler is an lx.Handler that stores log entries in memory. // Useful for testing or buffering logs for later inspection. // It maintains a thread-safe slice of log entries, protected by a read-write mutex. type MemoryHandler struct { mu sync.RWMutex // Protects concurrent access to entries entries []*lx.Entry // Slice of stored log entries showTime bool // Whether to show timestamps when dumping timeFormat string // Time format for dumping } // NewMemoryHandler creates a new MemoryHandler. // It initializes an empty slice for storing log entries, ready for use in logging or testing. // Example: // // handler := NewMemoryHandler() // logger := ll.New("app").Enable().Handler(handler) // logger.Info("Test") // Stores entry in memory func NewMemoryHandler() *MemoryHandler { return &MemoryHandler{ entries: make([]*lx.Entry, 0), // Initialize empty slice for entries } } // Timestamped enables/disables timestamp display when dumping and optionally sets a time format. // Consistent with TextHandler and ColorizedHandler signature. // Example: // // handler.Timestamped(true) // Enable with default format // handler.Timestamped(true, time.StampMilli) // Enable with custom format // handler.Timestamped(false) // Disable func (h *MemoryHandler) Timestamped(enable bool, format ...string) { h.mu.Lock() defer h.mu.Unlock() h.showTime = enable if len(format) > 0 && format[0] != "" { h.timeFormat = format[0] } } // Handle stores the log entry in memory. // It appends the provided entry to the entries slice, ensuring thread-safety with a write lock. // Always returns nil, as it does not perform I/O operations. // Example: // // handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Stores entry func (h *MemoryHandler) Handle(entry *lx.Entry) error { h.mu.Lock() defer h.mu.Unlock() h.entries = append(h.entries, entry) // Append entry to slice return nil } // Entries returns a copy of the stored log entries. // It creates a new slice with copies of all entries, ensuring thread-safety with a read lock. // The returned slice is safe for external use without affecting the handler's internal state. // Example: // // entries := handler.Entries() // Returns copy of stored entries func (h *MemoryHandler) Entries() []*lx.Entry { h.mu.RLock() defer h.mu.RUnlock() entries := make([]*lx.Entry, len(h.entries)) // Create new slice for copy copy(entries, h.entries) // Copy entries to new slice return entries } // Reset clears all stored entries. // It truncates the entries slice to zero length, preserving capacity, using a write lock for thread-safety. // Example: // // handler.Reset() // Clears all stored entries func (h *MemoryHandler) Reset() { h.mu.Lock() defer h.mu.Unlock() h.entries = h.entries[:0] // Truncate slice to zero length } // Dump writes all stored log entries to the provided io.Writer in text format. // Entries are formatted as they would be by a TextHandler, including namespace, level, // message, and fields. Thread-safe with read lock. // Returns an error if writing fails. // Example: // // logger := ll.New("test", ll.WithHandler(NewMemoryHandler())).Enable() // logger.Info("Test message") // handler := logger.handler.(*MemoryHandler) // handler.Dump(os.Stdout) // Output: [test] INFO: Test message func (h *MemoryHandler) Dump(w io.Writer) error { h.mu.RLock() defer h.mu.RUnlock() // Create a temporary TextHandler to format entries tempHandler := NewTextHandler(w) tempHandler.Timestamped(h.showTime, h.timeFormat) // Process each entry through the TextHandler for _, entry := range h.entries { if err := tempHandler.Handle(entry); err != nil { return fmt.Errorf("failed to dump entry: %w", err) // Wrap and return write errors } } return nil } golang-github-olekukonko-ll-0.0.9/lh/multi.go000066400000000000000000000037231504320775200211310ustar00rootroot00000000000000package lh import ( "errors" "fmt" "github.com/olekukonko/ll/lx" ) // MultiHandler combines multiple handlers to process log entries concurrently. // It holds a list of lx.Handler instances and delegates each log entry to all handlers, // collecting any errors into a single combined error. // Thread-safe if the underlying handlers are thread-safe. type MultiHandler struct { Handlers []lx.Handler // List of handlers to process each log entry } // NewMultiHandler creates a new MultiHandler with the specified handlers. // It accepts a variadic list of handlers to be executed in order. // The returned handler processes log entries by passing them to each handler in sequence. // Example: // // textHandler := NewTextHandler(os.Stdout) // jsonHandler := NewJSONHandler(os.Stdout) // multi := NewMultiHandler(textHandler, jsonHandler) // logger := ll.New("app").Enable().Handler(multi) // logger.Info("Test") // Processed by both text and JSON handlers func NewMultiHandler(h ...lx.Handler) *MultiHandler { return &MultiHandler{ Handlers: h, // Initialize with provided handlers } } // Handle implements the Handler interface, calling Handle on each handler in sequence. // It collects any errors from handlers and combines them into a single error using errors.Join. // If no errors occur, it returns nil. Thread-safe if the underlying handlers are thread-safe. // Example: // // multi.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Calls Handle on all handlers func (h *MultiHandler) Handle(e *lx.Entry) error { var errs []error // Collect errors from handlers for i, handler := range h.Handlers { // Process entry with each handler if err := handler.Handle(e); err != nil { // fmt.Fprintf(os.Stderr, "MultiHandler error for handler %d: %v\n", i, err) // Wrap error with handler index for context errs = append(errs, fmt.Errorf("handler %d: %w", i, err)) } } // Combine errors into a single error, or return nil if no errors return errors.Join(errs...) } golang-github-olekukonko-ll-0.0.9/lh/slog.go000066400000000000000000000060321504320775200207370ustar00rootroot00000000000000package lh import ( "context" "github.com/olekukonko/ll/lx" "log/slog" ) // SlogHandler adapts a slog.Handler to implement lx.Handler. // It converts lx.Entry objects to slog.Record objects and delegates to an underlying // slog.Handler for processing, enabling compatibility with Go's standard slog package. // Thread-safe if the underlying slog.Handler is thread-safe. type SlogHandler struct { slogHandler slog.Handler // Underlying slog.Handler for processing log records } // NewSlogHandler creates a new SlogHandler wrapping the provided slog.Handler. // It initializes the handler with the given slog.Handler, allowing lx.Entry logs to be // processed by slog's logging infrastructure. // Example: // // slogText := slog.NewTextHandler(os.Stdout, nil) // handler := NewSlogHandler(slogText) // logger := ll.New("app").Enable().Handler(handler) // logger.Info("Test") // Output: level=INFO msg=Test namespace=app class=Text func NewSlogHandler(h slog.Handler) *SlogHandler { return &SlogHandler{slogHandler: h} } // Handle converts an lx.Entry to slog.Record and delegates to the slog.Handler. // It maps the entry's fields, level, namespace, class, and stack trace to slog attributes, // passing the resulting record to the underlying slog.Handler. // Returns an error if the slog.Handler fails to process the record. // Thread-safe if the underlying slog.Handler is thread-safe. // Example: // // handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Processes as slog record func (h *SlogHandler) Handle(e *lx.Entry) error { // Convert lx.LevelType to slog.Level level := toSlogLevel(e.Level) // Create a slog.Record with the entry's data record := slog.NewRecord( e.Timestamp, // time.Time for log timestamp level, // slog.Level for log severity e.Message, // string for log message 0, // pc (program counter, optional, not used) ) // Add standard fields as attributes record.AddAttrs( slog.String("namespace", e.Namespace), // Add namespace as string attribute slog.String("class", e.Class.String()), // Add class as string attribute ) // Add stack trace if present if len(e.Stack) > 0 { record.AddAttrs(slog.String("stack", string(e.Stack))) // Add stack trace as string } // Add custom fields for k, v := range e.Fields { record.AddAttrs(slog.Any(k, v)) // Add each field as a key-value attribute } // Handle the record with the underlying slog.Handler return h.slogHandler.Handle(context.Background(), record) } // toSlogLevel converts lx.LevelType to slog.Level. // It maps the logging levels used by the lx package to those used by slog, // defaulting to slog.LevelInfo for unknown levels. // Example (internal usage): // // level := toSlogLevel(lx.LevelDebug) // Returns slog.LevelDebug func toSlogLevel(level lx.LevelType) slog.Level { switch level { case lx.LevelDebug: return slog.LevelDebug case lx.LevelInfo: return slog.LevelInfo case lx.LevelWarn: return slog.LevelWarn case lx.LevelError: return slog.LevelError default: return slog.LevelInfo // Default for unknown levels } } golang-github-olekukonko-ll-0.0.9/lh/text.go000066400000000000000000000147351504320775200207700ustar00rootroot00000000000000package lh import ( "fmt" "github.com/olekukonko/ll/lx" "io" "sort" "strings" "time" ) // TextHandler is a handler that outputs log entries as plain text. // It formats log entries with namespace, level, message, fields, and optional stack traces, // writing the result to the provided writer. // Thread-safe if the underlying writer is thread-safe. type TextHandler struct { w io.Writer // Destination for formatted log output showTime bool // Whether to display timestamps timeFormat string // Format for timestamps (defaults to time.RFC3339) } // NewTextHandler creates a new TextHandler writing to the specified writer. // It initializes the handler with the given writer, suitable for outputs like stdout or files. // Example: // // handler := NewTextHandler(os.Stdout) // logger := ll.New("app").Enable().Handler(handler) // logger.Info("Test") // Output: [app] INFO: Test func NewTextHandler(w io.Writer) *TextHandler { return &TextHandler{ w: w, showTime: false, timeFormat: time.RFC3339, } } // Timestamped enables or disables timestamp display and optionally sets a custom time format. // If format is empty, defaults to RFC3339. // Example: // // handler := NewTextHandler(os.Stdout).TextWithTime(true, time.StampMilli) // // Output: Jan 02 15:04:05.000 [app] INFO: Test func (h *TextHandler) Timestamped(enable bool, format ...string) { h.showTime = enable if len(format) > 0 && format[0] != "" { h.timeFormat = format[0] } } // Handle processes a log entry and writes it as plain text. // It delegates to specialized methods based on the entry's class (Dump, Raw, or regular). // Returns an error if writing to the underlying writer fails. // Thread-safe if the writer is thread-safe. // Example: // // handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes "INFO: test" func (h *TextHandler) Handle(e *lx.Entry) error { // Special handling for dump output if e.Class == lx.ClassDump { return h.handleDumpOutput(e) } // Raw entries are written directly without formatting if e.Class == lx.ClassRaw { _, err := h.w.Write([]byte(e.Message)) return err } // Handle standard log entries return h.handleRegularOutput(e) } // handleRegularOutput handles normal log entries. // It formats the entry with namespace, level, message, fields, and stack trace (if present), // writing the result to the handler's writer. // Returns an error if writing fails. // Example (internal usage): // // h.handleRegularOutput(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes "INFO: test" func (h *TextHandler) handleRegularOutput(e *lx.Entry) error { var builder strings.Builder // Buffer for building formatted output // Add timestamp if enabled if h.showTime { builder.WriteString(e.Timestamp.Format(h.timeFormat)) builder.WriteString(lx.Space) } // Format namespace based on style switch e.Style { case lx.NestedPath: if e.Namespace != "" { // Split namespace into parts and format as [parent]→[child] parts := strings.Split(e.Namespace, lx.Slash) for i, part := range parts { builder.WriteString(lx.LeftBracket) builder.WriteString(part) builder.WriteString(lx.RightBracket) if i < len(parts)-1 { builder.WriteString(lx.Arrow) } } builder.WriteString(lx.Colon) builder.WriteString(lx.Space) } default: // FlatPath if e.Namespace != "" { // Format namespace as [parent/child] builder.WriteString(lx.LeftBracket) builder.WriteString(e.Namespace) builder.WriteString(lx.RightBracket) builder.WriteString(lx.Space) } } // Add level and message builder.WriteString(e.Level.String()) builder.WriteString(lx.Colon) builder.WriteString(lx.Space) builder.WriteString(e.Message) // Add fields in sorted order if len(e.Fields) > 0 { var keys []string for k := range e.Fields { keys = append(keys, k) } // Sort keys for consistent output sort.Strings(keys) builder.WriteString(lx.Space) builder.WriteString(lx.LeftBracket) for i, k := range keys { if i > 0 { builder.WriteString(lx.Space) } // Format field as key=value builder.WriteString(k) builder.WriteString("=") builder.WriteString(fmt.Sprint(e.Fields[k])) } builder.WriteString(lx.RightBracket) } // Add stack trace if present if len(e.Stack) > 0 { h.formatStack(&builder, e.Stack) } // Append newline for non-None levels if e.Level != lx.LevelNone { builder.WriteString(lx.Newline) } // Write formatted output to writer _, err := h.w.Write([]byte(builder.String())) return err } // handleDumpOutput specially formats hex dump output (plain text version). // It wraps the dump message with BEGIN/END separators for clarity. // Returns an error if writing fails. // Example (internal usage): // // h.handleDumpOutput(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61"}) // Writes "---- BEGIN DUMP ----\npos 00 hex: 61\n---- END DUMP ----\n" func (h *TextHandler) handleDumpOutput(e *lx.Entry) error { // For text handler, we just add a newline before dump output var builder strings.Builder // Buffer for building formatted output // Add timestamp if enabled if h.showTime { builder.WriteString(e.Timestamp.Format(h.timeFormat)) builder.WriteString(lx.Newline) } // Add separator lines and dump content builder.WriteString("---- BEGIN DUMP ----\n") builder.WriteString(e.Message) builder.WriteString("---- END DUMP ----\n") // Write formatted output to writer _, err := h.w.Write([]byte(builder.String())) return err } // formatStack formats a stack trace for plain text output. // It structures the stack trace with indentation and separators for readability, // including goroutine and function/file details. // Example (internal usage): // // h.formatStack(&builder, []byte("goroutine 1 [running]:\nmain.main()\n\tmain.go:10")) // Appends formatted stack trace func (h *TextHandler) formatStack(b *strings.Builder, stack []byte) { lines := strings.Split(string(stack), "\n") if len(lines) == 0 { return } // Start stack trace section b.WriteString("\n[stack]\n") // First line: goroutine b.WriteString(" ┌─ ") b.WriteString(lines[0]) b.WriteString("\n") // Iterate through remaining lines for i := 1; i < len(lines); i++ { line := strings.TrimSpace(lines[i]) if line == "" { continue } if strings.Contains(line, ".go") { // File path lines get extra indent b.WriteString(" ├ ") } else { // Function names b.WriteString(" │ ") } b.WriteString(line) b.WriteString("\n") } // End stack trace section b.WriteString(" └\n") } golang-github-olekukonko-ll-0.0.9/ll.go000066400000000000000000001246441504320775200200110ustar00rootroot00000000000000package ll import ( "bufio" "encoding/binary" "encoding/json" "fmt" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" "io" "math" "os" "reflect" "runtime" "strings" "sync" "sync/atomic" "time" ) // Logger manages logging configuration and behavior, encapsulating state such as enablement, // log level, namespaces, context fields, output style, handler, middleware, and formatting. // It is thread-safe, using a read-write mutex to protect concurrent access to its fields. type Logger struct { mu sync.RWMutex // Guards concurrent access to fields enabled bool // Determines if logging is enabled suspend bool // uses suspend path for most actions eg. skipping namespace checks level lx.LevelType // Minimum log level (e.g., Debug, Info, Warn, Error) namespaces *lx.Namespace // Manages namespace enable/disable states currentPath string // Current namespace path (e.g., "parent/child") context map[string]interface{} // Contextual fields included in all logs style lx.StyleType // Namespace formatting style (FlatPath or NestedPath) handler lx.Handler // Output handler for logs (e.g., text, JSON) middleware []Middleware // Middleware functions to process log entries prefix string // Prefix prepended to log messages indent int // Number of double spaces for message indentation stackBufferSize int // Buffer size for capturing stack traces separator string // Separator for namespace paths (e.g., "/") entries atomic.Int64 // Tracks total log entries sent to handler } // New creates a new Logger with the given namespace and optional configurations. // It initializes with defaults: disabled, Debug level, flat namespace style, text handler // to os.Stdout, and an empty middleware chain. Options (e.g., WithHandler, WithLevel) can // override defaults. The logger is thread-safe via mutex-protected methods. // Example: // // logger := New("app", WithHandler(lh.NewTextHandler(os.Stdout))).Enable() // logger.Info("Starting application") // Output: [app] INFO: Starting application func New(namespace string, opts ...Option) *Logger { logger := &Logger{ enabled: lx.DefaultEnabled, // Defaults to disabled (false) level: lx.LevelDebug, // Default minimum log level namespaces: defaultStore, // Shared namespace store currentPath: namespace, // Initial namespace path context: make(map[string]interface{}), // Empty context for fields style: lx.FlatPath, // Default namespace style ([parent/child]) handler: lh.NewTextHandler(os.Stdout), // Default text output to stdout middleware: make([]Middleware, 0), // Empty middleware chain stackBufferSize: 4096, // Default stack trace buffer size separator: lx.Slash, // Default namespace separator ("/") } // Apply provided configuration options for _, opt := range opts { opt(logger) } return logger } // AddContext adds a key-value pair to the logger's context, modifying it directly. // Unlike Context, it mutates the existing context. It is thread-safe using a write lock. // Example: // // logger := New("app").Enable() // logger.AddContext("user", "alice") // logger.Info("Action") // Output: [app] INFO: Action [user=alice] func (l *Logger) AddContext(key string, value interface{}) *Logger { l.mu.Lock() defer l.mu.Unlock() // Initialize context map if nil if l.context == nil { l.context = make(map[string]interface{}) } l.context[key] = value return l } // Benchmark logs the duration since a start time at Info level, including "start", // "end", and "duration" fields. It is thread-safe via Fields and log methods. // Example: // // logger := New("app").Enable() // start := time.Now() // logger.Benchmark(start) // Output: [app] INFO: benchmark [start=... end=... duration=...] func (l *Logger) Benchmark(start time.Time) time.Duration { duration := time.Since(start) l.Fields("start", start, "end", time.Now(), "duration", duration).Infof("benchmark") return duration } // CanLog checks if a log at the given level would be emitted, considering enablement, // log level, namespaces, sampling, and rate limits. It is thread-safe via shouldLog. // Example: // // logger := New("app").Enable().Level(lx.LevelWarn) // canLog := logger.CanLog(lx.LevelInfo) // false func (l *Logger) CanLog(level lx.LevelType) bool { return l.shouldLog(level) } // Clear removes all middleware functions, resetting the middleware chain to empty. // It is thread-safe using a write lock and returns the logger for chaining. // Example: // // logger := New("app").Enable().Use(someMiddleware) // logger.Clear() // logger.Info("No middleware") // Output: [app] INFO: No middleware func (l *Logger) Clear() *Logger { l.mu.Lock() defer l.mu.Unlock() l.middleware = nil return l } // Clone creates a new logger with the same configuration and namespace as the parent, // but with a fresh context map to allow independent field additions. It is thread-safe // using a read lock. // Example: // // logger := New("app").Enable().Context(map[string]interface{}{"k": "v"}) // clone := logger.Clone() // clone.Info("Cloned") // Output: [app] INFO: Cloned [k=v] func (l *Logger) Clone() *Logger { l.mu.RLock() defer l.mu.RUnlock() return &Logger{ enabled: l.enabled, // Copy enablement state level: l.level, // Copy log level namespaces: l.namespaces, // Share namespace store currentPath: l.currentPath, // Copy namespace path context: make(map[string]interface{}), // Fresh context map style: l.style, // Copy namespace style handler: l.handler, // Copy output handler middleware: l.middleware, // Copy middleware chain prefix: l.prefix, // Copy message prefix indent: l.indent, // Copy indentation level stackBufferSize: l.stackBufferSize, // Copy stack trace buffer size separator: l.separator, // Default separator ("/") suspend: l.suspend, } } // Context creates a new logger with additional contextual fields, preserving existing // fields and adding new ones. It returns a new logger to avoid mutating the parent and // is thread-safe using a write lock. // Example: // // logger := New("app").Enable() // logger = logger.Context(map[string]interface{}{"user": "alice"}) // logger.Info("Action") // Output: [app] INFO: Action [user=alice] func (l *Logger) Context(fields map[string]interface{}) *Logger { l.mu.Lock() defer l.mu.Unlock() // Create a new logger with inherited configuration newLogger := &Logger{ enabled: l.enabled, level: l.level, namespaces: l.namespaces, currentPath: l.currentPath, context: make(map[string]interface{}), style: l.style, handler: l.handler, middleware: l.middleware, prefix: l.prefix, indent: l.indent, stackBufferSize: l.stackBufferSize, separator: l.separator, suspend: l.suspend, } // Copy parent's context fields for k, v := range l.context { newLogger.context[k] = v } // Add new fields for k, v := range fields { newLogger.context[k] = v } return newLogger } // Dbg logs debug information, including the source file, line number, and expression // value, capturing the calling line of code. It is useful for debugging without temporary // print statements. // Example: // // x := 42 // logger.Dbg(x) // Output: [file.go:123] x = 42 func (l *Logger) Dbg(values ...interface{}) { // Skip logging if Info level is not enabled if !l.shouldLog(lx.LevelInfo) { return } l.dbg(2, values...) } // Debug logs a message at Debug level, formatting it and delegating to the internal // log method. It is thread-safe. // Example: // // logger := New("app").Enable().Level(lx.LevelDebug) // logger.Debug("Debugging") // Output: [app] DEBUG: Debugging func (l *Logger) Debug(args ...any) { // check if suspended if l.suspend { return } // Skip logging if Debug level is not enabled if !l.shouldLog(lx.LevelDebug) { return } l.log(lx.LevelDebug, lx.ClassText, concatSpaced(args...), nil, false) } // Debugf logs a formatted message at Debug level, delegating to Debug. It is thread-safe. // Example: // // logger := New("app").Enable().Level(lx.LevelDebug) // logger.Debugf("Debug %s", "message") // Output: [app] DEBUG: Debug message func (l *Logger) Debugf(format string, args ...any) { // check if suspended if l.suspend { return } l.Debug(fmt.Sprintf(format, args...)) } // Disable deactivates logging, suppressing all logs regardless of level or namespace. // It is thread-safe using a write lock and returns the logger for chaining. // Example: // // logger := New("app").Enable().Disable() // logger.Info("Ignored") // No output func (l *Logger) Disable() *Logger { l.mu.Lock() defer l.mu.Unlock() l.enabled = false return l } // Dump displays a hex and ASCII representation of a value's binary form, using gob // encoding or direct conversion. It is useful for inspecting binary data structures. // Example: // // type Data struct { X int; Y string } // logger.Dump(Data{42, "test"}) // Outputs hex/ASCII dump func (l *Logger) Dump(values ...interface{}) { // Iterate over each value to dump for _, value := range values { // Log value description and type l.Infof("Dumping %v (%T)", value, value) var by []byte var err error // Convert value to byte slice based on type switch v := value.(type) { case []byte: by = v case string: by = []byte(v) case float32: // Convert float32 to 4-byte big-endian buf := make([]byte, 4) binary.BigEndian.PutUint32(buf, math.Float32bits(v)) by = buf case float64: // Convert float64 to 8-byte big-endian buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, math.Float64bits(v)) by = buf case int, int8, int16, int32, int64: // Convert signed integer to 8-byte big-endian by = make([]byte, 8) binary.BigEndian.PutUint64(by, uint64(reflect.ValueOf(v).Int())) case uint, uint8, uint16, uint32, uint64: // Convert unsigned integer to 8-byte big-endian by = make([]byte, 8) binary.BigEndian.PutUint64(by, reflect.ValueOf(v).Uint()) case io.Reader: // Read all bytes from io.Reader by, err = io.ReadAll(v) default: // Fallback to JSON marshaling for complex types by, err = json.Marshal(v) } // Log error if conversion fails if err != nil { l.Errorf("Dump error: %v", err) continue } // Generate hex/ASCII dump n := len(by) rowcount := 0 stop := (n / 8) * 8 k := 0 s := strings.Builder{} // Process 8-byte rows for i := 0; i <= stop; i += 8 { k++ if i+8 < n { rowcount = 8 } else { rowcount = min(k*8, n) % 8 } // Write position and hex prefix s.WriteString(fmt.Sprintf("pos %02d hex: ", i)) // Write hex values for j := 0; j < rowcount; j++ { s.WriteString(fmt.Sprintf("%02x ", by[i+j])) } // Pad with spaces for alignment for j := rowcount; j < 8; j++ { s.WriteString(fmt.Sprintf(" ")) } // Write ASCII representation s.WriteString(fmt.Sprintf(" '%s'\n", viewString(by[i:(i+rowcount)]))) } // Log the hex/ASCII dump l.log(lx.LevelNone, lx.ClassDump, s.String(), nil, false) } } // Enable activates logging, allowing logs to be emitted if other conditions (e.g., level, // namespace) are met. It is thread-safe using a write lock and returns the logger for chaining. // Example: // // logger := New("app").Enable() // logger.Info("Started") // Output: [app] INFO: Started func (l *Logger) Enable() *Logger { l.mu.Lock() defer l.mu.Unlock() l.enabled = true return l } // Enabled checks if the logger is enabled for logging. It is thread-safe using a read lock. // Example: // // logger := New("app").Enable() // if logger.Enabled() { // logger.Info("Logging is enabled") // Output: [app] INFO: Logging is enabled // } func (l *Logger) Enabled() bool { l.mu.RLock() defer l.mu.RUnlock() return l.enabled } // Err adds one or more errors to the logger’s context and logs them at Error level. // Non-nil errors are stored in the "error" context field (single error or slice) and // logged as a concatenated string (e.g., "failed 1; failed 2"). It is thread-safe and // returns the logger for chaining. // Example: // // logger := New("app").Enable() // err1 := errors.New("failed 1") // err2 := errors.New("failed 2") // logger.Err(err1, err2).Info("Error occurred") // // Output: [app] ERROR: failed 1; failed 2 // // [app] INFO: Error occurred [error=[failed 1 failed 2]] func (l *Logger) Err(errs ...error) { // Skip logging if Error level is not enabled if !l.shouldLog(lx.LevelError) { return } l.mu.Lock() // Initialize context map if nil if l.context == nil { l.context = make(map[string]interface{}) } // Collect non-nil errors and build log message var nonNilErrors []error var builder strings.Builder count := 0 for i, err := range errs { if err != nil { if i > 0 && count > 0 { builder.WriteString("; ") } builder.WriteString(err.Error()) nonNilErrors = append(nonNilErrors, err) count++ } } if count > 0 { if count == 1 { // Store single error directly l.context["error"] = nonNilErrors[0] } else { // Store slice of errors l.context["error"] = nonNilErrors } // Log concatenated error messages l.log(lx.LevelError, lx.ClassText, builder.String(), nil, false) } l.mu.Unlock() } // Error logs a message at Error level, formatting it and delegating to the internal // log method. It is thread-safe. // Example: // // logger := New("app").Enable() // logger.Error("Error occurred") // Output: [app] ERROR: Error occurred func (l *Logger) Error(args ...any) { // check if suspended if l.suspend { return } // Skip logging if Error level is not enabled if !l.shouldLog(lx.LevelError) { return } l.log(lx.LevelError, lx.ClassText, concatSpaced(args...), nil, false) } // Errorf logs a formatted message at Error level, delegating to Error. It is thread-safe. // Example: // // logger := New("app").Enable() // logger.Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred func (l *Logger) Errorf(format string, args ...any) { // check if suspended if l.suspend { return } l.Error(fmt.Errorf(format, args...)) } // Fatal logs a message at Error level with a stack trace and exits the program with // exit code 1. It is thread-safe. // Example: // // logger := New("app").Enable() // logger.Fatal("Fatal error") // Output: [app] ERROR: Fatal error [stack=...], then exits func (l *Logger) Fatal(args ...any) { // check if suspended if l.suspend { return } // Exit immediately if Error level is not enabled if !l.shouldLog(lx.LevelError) { os.Exit(1) } l.log(lx.LevelError, lx.ClassText, concatSpaced(args...), nil, true) os.Exit(1) } // Fatalf logs a formatted message at Error level with a stack trace and exits the program. // It delegates to Fatal and is thread-safe. // Example: // // logger := New("app").Enable() // logger.Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [stack=...], then exits func (l *Logger) Fatalf(format string, args ...any) { // check if suspended if l.suspend { return } l.Fatal(fmt.Sprintf(format, args...)) } // Field starts a fluent chain for adding fields from a map, creating a FieldBuilder // for type-safe field addition. It is thread-safe via the FieldBuilder’s logger. // Example: // // logger := New("app").Enable() // logger.Field(map[string]interface{}{"user": "alice"}).Info("Action") // Output: [app] INFO: Action [user=alice] func (l *Logger) Field(fields map[string]interface{}) *FieldBuilder { fb := &FieldBuilder{logger: l, fields: make(map[string]interface{})} // check if suspended if l.suspend { return fb } // Copy fields from input map to FieldBuilder for k, v := range fields { fb.fields[k] = v } return fb } // Fields starts a fluent chain for adding fields using variadic key-value pairs, // creating a FieldBuilder. Non-string keys or uneven pairs add an error field. It is // thread-safe via the FieldBuilder’s logger. // Example: // // logger := New("app").Enable() // logger.Fields("user", "alice").Info("Action") // Output: [app] INFO: Action [user=alice] func (l *Logger) Fields(pairs ...any) *FieldBuilder { fb := &FieldBuilder{logger: l, fields: make(map[string]interface{})} if l.suspend { return fb } // Process key-value pairs for i := 0; i < len(pairs)-1; i += 2 { if key, ok := pairs[i].(string); ok { fb.fields[key] = pairs[i+1] } else { // Log error for non-string keys fb.fields["error"] = fmt.Errorf("non-string key in Fields: %v", pairs[i]) } } // Log error for uneven pairs if len(pairs)%2 != 0 { fb.fields["error"] = fmt.Errorf("uneven key-value pairs in Fields: [%v]", pairs[len(pairs)-1]) } return fb } // GetContext returns the logger's context map of persistent key-value fields. It is // thread-safe using a read lock. // Example: // // logger := New("app").AddContext("user", "alice") // ctx := logger.GetContext() // Returns map[string]interface{}{"user": "alice"} func (l *Logger) GetContext() map[string]interface{} { l.mu.RLock() defer l.mu.RUnlock() return l.context } // GetHandler returns the logger's current handler for customization or inspection. // The returned handler should not be modified concurrently with logger operations. // Example: // // logger := New("app") // handler := logger.GetHandler() // Returns the current handler (e.g., TextHandler) func (l *Logger) GetHandler() lx.Handler { return l.handler } // GetLevel returns the minimum log level for the logger. It is thread-safe using a read lock. // Example: // // logger := New("app").Level(lx.LevelWarn) // if logger.GetLevel() == lx.LevelWarn { // logger.Warn("Warning level set") // Output: [app] WARN: Warning level set // } func (l *Logger) GetLevel() lx.LevelType { l.mu.RLock() defer l.mu.RUnlock() return l.level } // GetPath returns the logger's current namespace path. It is thread-safe using a read lock. // Example: // // logger := New("app").Namespace("sub") // path := logger.GetPath() // Returns "app/sub" func (l *Logger) GetPath() string { l.mu.RLock() defer l.mu.RUnlock() return l.currentPath } // GetSeparator returns the logger's namespace separator (e.g., "/"). It is thread-safe // using a read lock. // Example: // // logger := New("app").Separator(".") // sep := logger.GetSeparator() // Returns "." func (l *Logger) GetSeparator() string { l.mu.RLock() defer l.mu.RUnlock() return l.separator } // GetStyle returns the logger's namespace formatting style (FlatPath or NestedPath). // It is thread-safe using a read lock. // Example: // // logger := New("app").Style(lx.NestedPath) // if logger.GetStyle() == lx.NestedPath { // logger.Info("Nested style") // Output: [app]: INFO: Nested style // } func (l *Logger) GetStyle() lx.StyleType { l.mu.RLock() defer l.mu.RUnlock() return l.style } // Handler sets the handler for processing log entries, configuring the output destination // and format (e.g., text, JSON). It is thread-safe using a write lock and returns the // logger for chaining. // Example: // // logger := New("app").Enable().Handler(lh.NewTextHandler(os.Stdout)) // logger.Info("Log") // Output: [app] INFO: Log func (l *Logger) Handler(handler lx.Handler) *Logger { l.mu.Lock() defer l.mu.Unlock() l.handler = handler return l } // Indent sets the indentation level for log messages, adding two spaces per level. It is // thread-safe using a write lock and returns the logger for chaining. // Example: // // logger := New("app").Enable().Indent(2) // logger.Info("Indented") // Output: [app] INFO: Indented func (l *Logger) Indent(depth int) *Logger { l.mu.Lock() defer l.mu.Unlock() l.indent = depth return l } // Info logs a message at Info level, formatting it and delegating to the internal log // method. It is thread-safe. // Example: // // logger := New("app").Enable().Style(lx.NestedPath) // logger.Info("Started") // Output: [app]: INFO: Started func (l *Logger) Info(args ...any) { if l.suspend { return } if !l.shouldLog(lx.LevelInfo) { return } l.log(lx.LevelInfo, lx.ClassText, concatSpaced(args...), nil, false) } // Infof logs a formatted message at Info level, delegating to Info. It is thread-safe. // Example: // // logger := New("app").Enable().Style(lx.NestedPath) // logger.Infof("Started %s", "now") // Output: [app]: INFO: Started now func (l *Logger) Infof(format string, args ...any) { if l.suspend { return } l.Info(fmt.Sprintf(format, args...)) } // Len returns the total number of log entries sent to the handler, using atomic operations // for thread safety. // Example: // // logger := New("app").Enable() // logger.Info("Test") // count := logger.Len() // Returns 1 func (l *Logger) Len() int64 { return l.entries.Load() } // Level sets the minimum log level, ignoring messages below it. It is thread-safe using // a write lock and returns the logger for chaining. // Example: // // logger := New("app").Enable().Level(lx.LevelWarn) // logger.Info("Ignored") // No output // logger.Warn("Logged") // Output: [app] WARN: Logged func (l *Logger) Level(level lx.LevelType) *Logger { l.mu.Lock() defer l.mu.Unlock() l.level = level return l } // Line adds vertical spacing (newlines) to the log output, defaulting to 1 if no arguments // are provided. Multiple values are summed for total lines. It is thread-safe and returns // the logger for chaining. // Example: // // logger := New("app").Enable() // logger.Line(2).Info("After 2 newlines") // Adds 2 blank lines before logging // logger.Line().Error("After 1 newline") // Defaults to 1 func (l *Logger) Line(lines ...int) *Logger { line := 1 // Default to 1 newline if len(lines) > 0 { line = 0 // Sum all provided line counts for _, n := range lines { line += n } // Ensure at least 1 line if line < 1 { line = 1 } } l.log(lx.LevelNone, lx.ClassRaw, strings.Repeat(lx.Newline, line), nil, false) return l } // Mark logs the current file and line number where it's called, without any additional debug information. // It's useful for tracing execution flow without the verbosity of Dbg. // Example: // // logger.Mark() // *MARK*: [file.go:123] func (l *Logger) Mark(name ...string) { l.mark(2, name...) } func (l *Logger) mark(skip int, names ...string) { // Skip logging if Info level is not enabled if !l.shouldLog(lx.LevelInfo) { return } // Get caller information (file, line) _, file, line, ok := runtime.Caller(skip) if !ok { l.log(lx.LevelError, lx.ClassText, "Mark: Unable to parse runtime caller", nil, false) return } // Extract just the filename (without full path) shortFile := file if idx := strings.LastIndex(file, "/"); idx >= 0 { shortFile = file[idx+1:] } name := strings.Join(names, l.separator) if name == "" { name = "MARK" } // Format as [filename:line] out := fmt.Sprintf("[*%s*]: [%s:%d]\n", name, shortFile, line) l.log(lx.LevelInfo, lx.ClassRaw, out, nil, false) } // Measure benchmarks function execution, logging the duration at Info level with a // "duration" field. It is thread-safe via Fields and log methods. // Example: // // logger := New("app").Enable() // duration := logger.Measure(func() { time.Sleep(time.Millisecond) }) // // Output: [app] INFO: function executed [duration=~1ms] func (l *Logger) Measure(fns ...func()) time.Duration { start := time.Now() // Execute all provided functions for _, fn := range fns { fn() } duration := time.Since(start) l.Fields("duration", duration).Infof("function executed") return duration } // Namespace creates a child logger with a sub-namespace appended to the current path, // inheriting the parent’s configuration but with an independent context. It is thread-safe // using a read lock. // Example: // // parent := New("parent").Enable() // child := parent.Namespace("child") // child.Info("Child log") // Output: [parent/child] INFO: Child log func (l *Logger) Namespace(name string) *Logger { if l.suspend { return l } l.mu.RLock() defer l.mu.RUnlock() // Construct full namespace path fullPath := name if l.currentPath != "" { fullPath = l.currentPath + l.separator + name } // Create child logger with inherited configuration return &Logger{ enabled: l.enabled, level: l.level, namespaces: l.namespaces, currentPath: fullPath, context: make(map[string]interface{}), style: l.style, handler: l.handler, middleware: l.middleware, prefix: l.prefix, indent: l.indent, stackBufferSize: l.stackBufferSize, separator: l.separator, suspend: l.suspend, } } // NamespaceDisable disables logging for a namespace and its children, invalidating the // namespace cache. It is thread-safe via lx.Namespace’s sync.Map and returns the logger // for chaining. // Example: // // logger := New("parent").Enable().NamespaceDisable("parent/child") // logger.Namespace("child").Info("Ignored") // No output func (l *Logger) NamespaceDisable(relativePath string) *Logger { l.mu.RLock() fullPath := l.joinPath(l.currentPath, relativePath) l.mu.RUnlock() // Disable namespace in shared store l.namespaces.Set(fullPath, false) return l } // NamespaceEnable enables logging for a namespace and its children, invalidating the // namespace cache. It is thread-safe via lx.Namespace’s sync.Map and returns the logger // for chaining. // Example: // // logger := New("parent").Enable().NamespaceEnable("parent/child") // logger.Namespace("child").Info("Log") // Output: [parent/child] INFO: Log func (l *Logger) NamespaceEnable(relativePath string) *Logger { l.mu.RLock() fullPath := l.joinPath(l.currentPath, relativePath) l.mu.RUnlock() // Enable namespace in shared store l.namespaces.Set(fullPath, true) return l } // NamespaceEnabled checks if a namespace is enabled, considering parent namespaces and // caching results for performance. It is thread-safe using a read lock. // Example: // // logger := New("parent").Enable().NamespaceDisable("parent/child") // enabled := logger.NamespaceEnabled("parent/child") // false func (l *Logger) NamespaceEnabled(relativePath string) bool { l.mu.RLock() fullPath := l.joinPath(l.currentPath, relativePath) separator := l.separator if separator == "" { separator = lx.Slash } instanceEnabled := l.enabled l.mu.RUnlock() // Handle root path case if fullPath == "" && relativePath == "" { return instanceEnabled } if fullPath != "" { // Check namespace rules isEnabledByNSRule, isDisabledByNSRule := l.namespaces.Enabled(fullPath, separator) if isDisabledByNSRule { return false } if isEnabledByNSRule { return true } } // Fall back to logger's enabled state return instanceEnabled } // Panic logs a message at Error level with a stack trace and triggers a panic. It is // thread-safe. // Example: // // logger := New("app").Enable() // logger.Panic("Panic error") // Output: [app] ERROR: Panic error [stack=...], then panics func (l *Logger) Panic(args ...any) { // Build message by concatenating arguments with spaces msg := concatSpaced(args...) if l.suspend { panic(msg) } // Panic immediately if Error level is not enabled if !l.shouldLog(lx.LevelError) { panic(msg) } l.log(lx.LevelError, lx.ClassText, msg, nil, true) panic(msg) } // Panicf logs a formatted message at Error level with a stack trace and triggers a panic. // It delegates to Panic and is thread-safe. // Example: // // logger := New("app").Enable() // logger.Panicf("Panic %s", "error") // Output: [app] ERROR: Panic error [stack=...], then panics func (l *Logger) Panicf(format string, args ...any) { l.Panic(fmt.Sprintf(format, args...)) } // Prefix sets a prefix prepended to all log messages. It is thread-safe using a write // lock and returns the logger for chaining. // Example: // // logger := New("app").Enable().Prefix("APP: ") // logger.Info("Started") // Output: [app] INFO: APP: Started func (l *Logger) Prefix(prefix string) *Logger { l.mu.Lock() defer l.mu.Unlock() l.prefix = prefix return l } // Print logs a message at Info level without format specifiers, minimizing allocations // by concatenating arguments with spaces. It is thread-safe via the log method. // Example: // // logger := New("app").Enable() // logger.Print("message", "value") // Output: [app] INFO: message value func (l *Logger) Print(args ...any) { if l.suspend { return } // Skip logging if Info level is not enabled if !l.shouldLog(lx.LevelInfo) { return } l.log(lx.LevelNone, lx.ClassRaw, concatSpaced(args...), nil, false) } // Println logs a message at Info level without format specifiers, minimizing allocations // by concatenating arguments with spaces. It is thread-safe via the log method. // Example: // // logger := New("app").Enable() // logger.Println("message", "value") // Output: [app] INFO: message value func (l *Logger) Println(args ...any) { if l.suspend { return } // Skip logging if Info level is not enabled if !l.shouldLog(lx.LevelInfo) { return } l.log(lx.LevelNone, lx.ClassRaw, concatenate(lx.Space, nil, []any{lx.Newline}, args...), nil, false) } // Printf logs a formatted message at Info level, delegating to Print. It is thread-safe. // Example: // // logger := New("app").Enable() // logger.Printf("Message %s", "value") // Output: [app] INFO: Message value func (l *Logger) Printf(format string, args ...any) { if l.suspend { return } l.Print(fmt.Sprintf(format, args...)) } // Remove removes middleware by the reference returned from Use, delegating to the // Middleware’s Remove method for thread-safe removal. // Example: // // logger := New("app").Enable() // mw := logger.Use(someMiddleware) // logger.Remove(mw) // Removes middleware func (l *Logger) Remove(m *Middleware) { m.Remove() } // Resume reactivates logging for the current logger after it has been suspended. // It clears the suspend flag, allowing logs to be emitted if other conditions (e.g., level, namespace) // are met. Thread-safe with a write lock. Returns the logger for method chaining. // Example: // // logger := New("app").Enable().Suspend() // logger.Resume() // logger.Info("Resumed") // Output: [app] INFO: Resumed func (l *Logger) Resume() *Logger { l.mu.Lock() defer l.mu.Unlock() l.suspend = false // Clear suspend flag to resume logging return l } // Separator sets the namespace separator for grouping namespaces and log entries (e.g., "/" or "."). // It is thread-safe using a write lock and returns the logger for chaining. // Example: // // logger := New("app").Separator(".") // logger.Namespace("sub").Info("Log") // Output: [app.sub] INFO: Log func (l *Logger) Separator(separator string) *Logger { l.mu.Lock() defer l.mu.Unlock() l.separator = separator return l } // Suspend temporarily deactivates logging for the current logger. // It sets the suspend flag, suppressing all logs regardless of level or namespace until resumed. // Thread-safe with a write lock. Returns the logger for method chaining. // Example: // // logger := New("app").Enable() // logger.Suspend() // logger.Info("Ignored") // No output func (l *Logger) Suspend() *Logger { l.mu.Lock() defer l.mu.Unlock() l.suspend = true // Set suspend flag to pause logging return l } // Suspended returns whether the logger is currently suspended. // It provides thread-safe read access to the suspend flag using a write lock. // Example: // // logger := New("app").Enable().Suspend() // if logger.Suspended() { // fmt.Println("Logging is suspended") // Prints message // } func (l *Logger) Suspended() bool { l.mu.Lock() defer l.mu.Unlock() return l.suspend // Return current suspend state } // Stack logs messages at Error level with a stack trace for each provided argument. // It is thread-safe and skips logging if Debug level is not enabled. // Example: // // logger := New("app").Enable() // logger.Stack("Critical error") // Output: [app] ERROR: Critical error [stack=...] func (l *Logger) Stack(args ...any) { if l.suspend { return } // Skip logging if Debug level is not enabled if !l.shouldLog(lx.LevelDebug) { return } for _, arg := range args { l.log(lx.LevelError, lx.ClassText, concat(arg), nil, true) } } // Stackf logs a formatted message at Error level with a stack trace, delegating to Stack. // It is thread-safe. // Example: // // logger := New("app").Enable() // logger.Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [stack=...] func (l *Logger) Stackf(format string, args ...any) { if l.suspend { return } l.Stack(fmt.Sprintf(format, args...)) } // StackSize sets the buffer size for stack trace capture in Stack, Fatal, and Panic methods. // It is thread-safe using a write lock and returns the logger for chaining. // Example: // // logger := New("app").Enable().StackSize(65536) // logger.Stack("Error") // Captures up to 64KB stack trace func (l *Logger) StackSize(size int) *Logger { l.mu.Lock() defer l.mu.Unlock() if size > 0 { l.stackBufferSize = size } return l } // Style sets the namespace formatting style (FlatPath or NestedPath). FlatPath uses // [parent/child], while NestedPath uses [parent]→[child]. It is thread-safe using a write // lock and returns the logger for chaining. // Example: // // logger := New("parent/child").Enable().Style(lx.NestedPath) // logger.Info("Log") // Output: [parent]→[child]: INFO: Log func (l *Logger) Style(style lx.StyleType) *Logger { l.mu.Lock() defer l.mu.Unlock() l.style = style return l } // Timestamped enables or disables timestamp logging for the logger and optionally sets the timestamp format. // It is thread-safe, using a write lock to ensure safe concurrent access. // If the logger's handler supports the lx.Timestamper interface, the timestamp settings are applied. // The method returns the logger instance to support method chaining. // Parameters: // // enable: Boolean to enable or disable timestamp logging // format: Optional string(s) to specify the timestamp format func (l *Logger) Timestamped(enable bool, format ...string) *Logger { l.mu.Lock() defer l.mu.Unlock() if h, ok := l.handler.(lx.Timestamper); ok { h.Timestamped(enable, format...) } return l } // Use adds a middleware function to process log entries before they are handled, returning // a Middleware handle for removal. Middleware returning a non-nil error stops the log. // It is thread-safe using a write lock. // Example: // // logger := New("app").Enable() // mw := logger.Use(ll.FuncMiddleware(func(e *lx.Entry) error { // if e.Level < lx.LevelWarn { // return fmt.Errorf("level too low") // } // return nil // })) // logger.Info("Ignored") // No output // mw.Remove() // logger.Info("Now logged") // Output: [app] INFO: Now logged func (l *Logger) Use(fn lx.Handler) *Middleware { l.mu.Lock() defer l.mu.Unlock() // Assign a unique ID to the middleware id := len(l.middleware) + 1 // Append middleware to the chain l.middleware = append(l.middleware, Middleware{id: id, fn: fn}) return &Middleware{ logger: l, id: id, } } // Warn logs a message at Warn level, formatting it and delegating to the internal log // method. It is thread-safe. // Example: // // logger := New("app").Enable() // logger.Warn("Warning") // Output: [app] WARN: Warning func (l *Logger) Warn(args ...any) { if l.suspend { return } // Skip logging if Warn level is not enabled if !l.shouldLog(lx.LevelWarn) { return } l.log(lx.LevelWarn, lx.ClassText, concatSpaced(args...), nil, false) } // Warnf logs a formatted message at Warn level, delegating to Warn. It is thread-safe. // Example: // // logger := New("app").Enable() // logger.Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued func (l *Logger) Warnf(format string, args ...any) { if l.suspend { return } l.Warn(fmt.Sprintf(format, args...)) } // dbg is an internal helper for Dbg, logging debug information with source file and line // number, extracting the calling line of code. It is thread-safe via the log method. // Example (internal usage): // // logger.Dbg(x) // Calls dbg(2, x) func (l *Logger) dbg(skip int, values ...interface{}) { for _, exp := range values { // Get caller information (file, line) _, file, line, ok := runtime.Caller(skip) if !ok { l.log(lx.LevelError, lx.ClassText, "Dbg: Unable to parse runtime caller", nil, false) return } // Open source file f, err := os.Open(file) if err != nil { l.log(lx.LevelError, lx.ClassText, "Dbg: Unable to open expected file", nil, false) return } // Scan file to find the line scanner := bufio.NewScanner(f) scanner.Split(bufio.ScanLines) var out string i := 1 for scanner.Scan() { if i == line { // Extract expression between parentheses v := scanner.Text()[strings.Index(scanner.Text(), "(")+1 : len(scanner.Text())-strings.Index(reverseString(scanner.Text()), ")")-1] // Format output with file, line, expression, and value out = fmt.Sprintf("[%s:%d] %s = %+v", file[len(file)-strings.Index(reverseString(file), "/"):], line, v, exp) break } i++ } if err := scanner.Err(); err != nil { l.log(lx.LevelError, lx.ClassText, err.Error(), nil, false) return } // Log based on value type switch exp.(type) { case error: l.log(lx.LevelError, lx.ClassText, out, nil, false) default: l.log(lx.LevelInfo, lx.ClassText, out, nil, false) } f.Close() } } // joinPath joins a base path and a relative path using the logger's separator, handling // empty base or relative paths. It is used internally for namespace path construction. // Example (internal usage): // // logger.joinPath("parent", "child") // Returns "parent/child" func (l *Logger) joinPath(base, relative string) string { if base == "" { return relative } if relative == "" { return base } separator := l.separator if separator == "" { separator = lx.Slash // Default separator } return base + separator + relative } // log is the internal method for processing a log entry, applying rate limiting, sampling, // middleware, and context before passing to the handler. Middleware returning a non-nil // error stops the log. It is thread-safe with read/write locks for configuration and stack // trace buffer. // Example (internal usage): // // logger := New("app").Enable() // logger.Info("Test") // Calls log(lx.LevelInfo, "Test", nil, false) func (l *Logger) log(level lx.LevelType, class lx.ClassType, msg string, fields map[string]interface{}, withStack bool) { // Skip logging if level is not enabled if !l.shouldLog(level) { return } var stack []byte // Capture stack trace if requested if withStack { l.mu.RLock() buf := make([]byte, l.stackBufferSize) l.mu.RUnlock() n := runtime.Stack(buf, false) if fields == nil { fields = make(map[string]interface{}) } stack = buf[:n] } l.mu.RLock() defer l.mu.RUnlock() // Apply prefix and indentation to the message var builder strings.Builder if l.indent > 0 { builder.WriteString(strings.Repeat(lx.DoubleSpace, l.indent)) } if l.prefix != "" { builder.WriteString(l.prefix) } builder.WriteString(msg) finalMsg := builder.String() // Create log entry entry := &lx.Entry{ Timestamp: time.Now(), Level: level, Message: finalMsg, Namespace: l.currentPath, Fields: fields, Style: l.style, Class: class, Stack: stack, } // Merge context fields, avoiding overwrites if len(l.context) > 0 { if entry.Fields == nil { entry.Fields = make(map[string]interface{}) } for k, v := range l.context { if _, exists := entry.Fields[k]; !exists { entry.Fields[k] = v } } } // Apply middleware, stopping if any returns an error for _, mw := range l.middleware { if err := mw.fn.Handle(entry); err != nil { return } } // Pass to handler if set if l.handler != nil { _ = l.handler.Handle(entry) l.entries.Add(1) } } // shouldLog determines if a log should be emitted based on enabled state, level, namespaces, // sampling, and rate limits, caching namespace results for performance. It is thread-safe // with a read lock. // Example (internal usage): // // logger := New("app").Enable().Level(lx.LevelWarn) // if logger.shouldLog(lx.LevelInfo) { // false // // Log would be skipped // } func (l *Logger) shouldLog(level lx.LevelType) bool { // Skip if global logging system is inactive if !Active() { return false } // check for suspend mode if l.suspend { return false } // Skip if log level is below minimum if level > l.level { return false } separator := l.separator if separator == "" { separator = lx.Slash } // Check namespace rules if path is set if l.currentPath != "" { isEnabledByNSRule, isDisabledByNSRule := l.namespaces.Enabled(l.currentPath, separator) if isDisabledByNSRule { return false } if isEnabledByNSRule { return true } } // Fall back to logger's enabled state if !l.enabled { return false } return true } // WithHandler sets the handler for the logger as a functional option for configuring // a new logger instance. // Example: // // logger := New("app", WithHandler(lh.NewJSONHandler(os.Stdout))) func WithHandler(handler lx.Handler) Option { return func(l *Logger) { l.handler = handler } } // WithTimestamped returns an Option that configures timestamp settings for the logger's existing handler. // It enables or disables timestamp logging and optionally sets the timestamp format if the handler // supports the lx.Timestamper interface. If no handler is set, the function has no effect. // Parameters: // // enable: Boolean to enable or disable timestamp logging // format: Optional string(s) to specify the timestamp format func WithTimestamped(enable bool, format ...string) Option { return func(l *Logger) { if l.handler != nil { // Check if a handler is set // Verify if the handler supports the lx.Timestamper interface if h, ok := l.handler.(lx.Timestamper); ok { h.Timestamped(enable, format...) // Apply timestamp settings to the handler } } } } // WithLevel sets the minimum log level for the logger as a functional option for // configuring a new logger instance. // Example: // // logger := New("app", WithLevel(lx.LevelWarn)) func WithLevel(level lx.LevelType) Option { return func(l *Logger) { l.level = level } } // WithStyle sets the namespace formatting style for the logger as a functional option // for configuring a new logger instance. // Example: // // logger := New("app", WithStyle(lx.NestedPath)) func WithStyle(style lx.StyleType) Option { return func(l *Logger) { l.style = style } } golang-github-olekukonko-ll-0.0.9/lm/000077500000000000000000000000001504320775200174505ustar00rootroot00000000000000golang-github-olekukonko-ll-0.0.9/lm/rate.go000066400000000000000000000067411504320775200207420ustar00rootroot00000000000000package lm import ( "fmt" "github.com/olekukonko/ll/lx" "sync" "time" ) // RateLimiter is a middleware that limits the rate of log entries per level. // It tracks log counts for each log level within a specified time interval, // rejecting entries that exceed the allowed rate. // Thread-safe with mutexes for concurrent access. type RateLimiter struct { limits map[lx.LevelType]*rateLimit // Map of log levels to their rate limits mu sync.Mutex // Protects concurrent access to limits map } // rateLimit holds rate limiting state for a specific log level. // It tracks the current log count, maximum allowed logs, time interval, // and the timestamp of the last log. type rateLimit struct { count int // Current number of logs in the interval maxCount int // Maximum allowed logs per interval interval time.Duration // Time window for rate limiting last time.Time // Time of the last log mu sync.Mutex // Protects concurrent access } // NewRateLimiter creates a new RateLimiter for a specific log level. // It initializes the limiter with the given level, maximum log count, and time interval. // The limiter can be extended to other levels using the Set method. // Example: // // limiter := NewRateLimiter(lx.LevelInfo, 10, time.Second) // logger := ll.New("app").Enable().Use(limiter) // logger.Info("Test") // Allowed up to 10 times per second func NewRateLimiter(level lx.LevelType, count int, interval time.Duration) *RateLimiter { r := &RateLimiter{ limits: make(map[lx.LevelType]*rateLimit), // Initialize empty limits map } // Set initial rate limit for the specified level r.Set(level, count, interval) return r } // Set configures a rate limit for a specific log level. // It adds or updates the rate limit for the given level with the specified count and interval. // Thread-safe with a mutex. Returns the RateLimiter for chaining. // Example: // // limiter := NewRateLimiter(lx.LevelInfo, 10, time.Second) // limiter.Set(lx.LevelWarn, 5, time.Minute) // Add limit for Warn level func (rl *RateLimiter) Set(level lx.LevelType, count int, interval time.Duration) *RateLimiter { rl.mu.Lock() defer rl.mu.Unlock() // Create or update rate limit for the level rl.limits[level] = &rateLimit{ count: 0, // Initialize count to zero maxCount: count, // Set maximum allowed logs interval: interval, // Set time window last: time.Now(), // Set initial timestamp } return rl } // Handle processes a log entry and enforces rate limiting. // It checks if the entry's level has a rate limit and verifies if the log count // within the current interval exceeds the maximum allowed. // Returns an error if the rate limit is exceeded, causing the log to be dropped. // Thread-safe with mutexes for concurrent access. // Example (internal usage): // // err := limiter.Handle(&lx.Entry{Level: lx.LevelInfo}) // Returns error if limit exceeded func (rl *RateLimiter) Handle(e *lx.Entry) error { rl.mu.Lock() limit, exists := rl.limits[e.Level] // Check if level has a rate limit rl.mu.Unlock() if !exists { return nil // No limit for this level, allow log } limit.mu.Lock() defer limit.mu.Unlock() now := time.Now() // Reset count if interval has passed if now.Sub(limit.last) >= limit.interval { limit.last = now limit.count = 0 } limit.count++ // Increment log count // Check if limit is exceeded if limit.count > limit.maxCount { return fmt.Errorf("rate limit exceeded") // Drop log } return nil // Allow log } golang-github-olekukonko-ll-0.0.9/lm/sampling.go000066400000000000000000000070061504320775200216140ustar00rootroot00000000000000package lm import ( "fmt" "github.com/olekukonko/ll/lx" "math/rand" "sync" ) // Sampling is a middleware that randomly samples log entries based on a rate per level. // It allows logs to pass through with a specified probability, tracking rejected logs in stats. // Thread-safe with a mutex for concurrent access to rates and stats maps. type Sampling struct { rates map[lx.LevelType]float64 // Sampling rates per log level (0.0 to 1.0) stats map[lx.LevelType]int // Count of rejected logs per level mu sync.Mutex // Protects concurrent access to rates and stats } // NewSampling creates a new Sampling middleware for a specific log level. // It initializes the middleware with a sampling rate for the given level, // allowing further configuration via the Set method. // Example: // // sampler := NewSampling(lx.LevelDebug, 0.1) // Sample 10% of Debug logs // logger := ll.New("app").Enable().Use(sampler) // logger.Debug("Test") // Passes with 10% probability func NewSampling(level lx.LevelType, rate float64) *Sampling { s := &Sampling{ rates: make(map[lx.LevelType]float64), // Initialize empty rates map stats: make(map[lx.LevelType]int), // Initialize empty stats map } // Set initial sampling rate for the specified level s.Set(level, rate) return s } // Set configures a sampling rate for a specific log level. // It adds or updates the sampling rate (0.0 to 1.0) for the given level, // where 0.0 rejects all logs and 1.0 allows all logs. // Thread-safe with a mutex. Returns the Sampling instance for chaining. // Example: // // sampler := NewSampling(lx.LevelDebug, 0.1) // sampler.Set(lx.LevelInfo, 0.5) // Sample 50% of Info logs func (s *Sampling) Set(level lx.LevelType, rate float64) *Sampling { s.mu.Lock() defer s.mu.Unlock() s.rates[level] = rate // Set or update sampling rate return s } // Handle processes a log entry and applies sampling based on the level's rate. // It generates a random number and compares it to the level's sampling rate, // allowing the log if the random number is less than or equal to the rate. // Rejected logs increment the stats counter. Returns an error for rejected logs. // Thread-safe with a mutex for stats updates. // Example (internal usage): // // err := sampler.Handle(&lx.Entry{Level: lx.LevelDebug}) // Returns error if rejected func (s *Sampling) Handle(e *lx.Entry) error { rate, exists := s.rates[e.Level] // Check if level has a sampling rate if !exists { // fmt.Printf("Sampling: No rate for level %v\n", e.Level) return nil // No sampling for this level, allow log } s.mu.Lock() defer s.mu.Unlock() random := rand.Float64() // Generate random number (0.0 to 1.0) if random <= rate { // fmt.Printf("Sampling: rate=%v, random=%v, allowing log\n", rate, random) return nil // Allow log based on sampling rate } s.stats[e.Level]++ // Increment rejected log count // fmt.Printf("Sampling: rate=%v, random=%v, rejecting log\n", rate, random) return fmt.Errorf("sampling error") // Reject log } // GetStats returns a copy of the sampling statistics. // It provides the count of rejected logs per level, ensuring thread-safety with a read lock. // The returned map is safe for external use without affecting internal state. // Example: // // stats := sampler.GetStats() // Returns map of rejected log counts by level func (s *Sampling) GetStats() map[lx.LevelType]int { s.mu.Lock() defer s.mu.Unlock() result := make(map[lx.LevelType]int) // Create new map for copy // Copy stats to new map for k, v := range s.stats { result[k] = v } return result } golang-github-olekukonko-ll-0.0.9/lx/000077500000000000000000000000001504320775200174635ustar00rootroot00000000000000golang-github-olekukonko-ll-0.0.9/lx/lx.go000066400000000000000000000160311504320775200204360ustar00rootroot00000000000000package lx import ( "time" ) // Formatting constants for log output. // These constants define the characters used to format log messages, ensuring consistency // across handlers (e.g., text, JSON, colorized). They are used to construct namespace paths, // level indicators, and field separators in log entries. const ( Space = " " // Single space for separating elements (e.g., between level and message) DoubleSpace = " " // Double space for indentation (e.g., for hierarchical output) Slash = "/" // Separator for namespace paths (e.g., "parent/child") Arrow = "→" // Arrow for NestedPath style namespaces (e.g., [parent]→[child]) LeftBracket = "[" // Opening bracket for namespaces and fields (e.g., [app]) RightBracket = "]" // Closing bracket for namespaces and fields (e.g., [app]) Colon = ":" // Separator after namespace or level (e.g., [app]: INFO:) Dot = "." // Separator for namespace paths (e.g., "parent.child") Newline = "\n" // Newline for separating log entries or stack trace lines ) // DefaultEnabled defines the default logging state (disabled). // It specifies whether logging is enabled by default for new Logger instances in the ll package. // Set to false to prevent logging until explicitly enabled. const ( DefaultEnabled = false // Default state for new loggers (disabled) ) // Log level constants, ordered by increasing severity. // These constants define the severity levels for log messages, used to filter logs based // on the logger’s minimum level. They are ordered to allow comparison (e.g., LevelDebug < LevelWarn). const ( LevelNone LevelType = iota // Debug level for detailed diagnostic information LevelInfo // Info level for general operational messages LevelWarn // Warn level for warning conditions LevelError // Error level for error conditions requiring attention LevelDebug // None level for logs without a specific severity (e.g., raw output) ) // Log class constants, defining the type of log entry. // These constants categorize log entries by their content or purpose, influencing how // handlers process them (e.g., text, JSON, hex dump). const ( ClassText ClassType = iota // Text entries for standard log messages ClassJSON // JSON entries for structured output ClassDump // Dump entries for hex/ASCII dumps ClassSpecial // Special entries for custom or non-standard logs ClassRaw // Raw entries for unformatted output ) // Namespace style constants. // These constants define how namespace paths are formatted in log output, affecting the // visual representation of hierarchical namespaces. const ( FlatPath StyleType = iota // Formats namespaces as [parent/child] NestedPath // Formats namespaces as [parent]→[child] ) // LevelType represents the severity of a log message. // It is an integer type used to define log levels (Debug, Info, Warn, Error, None), with associated // string representations for display in log output. type LevelType int // String converts a LevelType to its string representation. // It maps each level constant to a human-readable string, returning "UNKNOWN" for invalid levels. // Used by handlers to display the log level in output. // Example: // // var level lx.LevelType = lx.LevelInfo // fmt.Println(level.String()) // Output: INFO func (l LevelType) String() string { switch l { case LevelDebug: return "DEBUG" case LevelInfo: return "INFO" case LevelWarn: return "WARN" case LevelError: return "ERROR" case LevelNone: return "NONE" default: return "UNKNOWN" } } // StyleType defines how namespace paths are formatted in log output. // It is an integer type used to select between FlatPath ([parent/child]) and NestedPath // ([parent]→[child]) styles, affecting how handlers render namespace hierarchies. type StyleType int // Entry represents a single log entry passed to handlers. // It encapsulates all information about a log message, including its timestamp, severity, // content, namespace, metadata, and formatting style. Handlers process Entry instances // to produce formatted output (e.g., text, JSON). The struct is immutable once created, // ensuring thread-safety in handler processing. type Entry struct { Timestamp time.Time // Time the log was created Level LevelType // Severity level of the log (Debug, Info, Warn, Error, None) Message string // Log message content Namespace string // Namespace path (e.g., "parent/child") Fields map[string]interface{} // Additional key-value metadata (e.g., {"user": "alice"}) Style StyleType // Namespace formatting style (FlatPath or NestedPath) Error error // Associated error, if any (e.g., for error logs) Class ClassType // Type of log entry (Text, JSON, Dump, Special, Raw) Stack []byte // Stack trace data (if present) Id int `json:"-"` // Unique ID for the entry, ignored in JSON output } // Handler defines the interface for processing log entries. // Implementations (e.g., TextHandler, JSONHandler) format and output log entries to various // destinations (e.g., stdout, files). The Handle method returns an error if processing fails, // allowing the logger to handle output failures gracefully. // Example (simplified handler implementation): // // type MyHandler struct{} // func (h *MyHandler) Handle(e *Entry) error { // fmt.Printf("[%s] %s: %s\n", e.Namespace, e.Level.String(), e.Message) // return nil // } type Handler interface { Handle(e *Entry) error // Processes a log entry, returning any error } // Timestamper defines an interface for handlers that support timestamp configuration. // It includes a method to enable or disable timestamp logging and optionally set the timestamp format. type Timestamper interface { // Timestamped enables or disables timestamp logging and allows specifying an optional format. // Parameters: // enable: Boolean to enable or disable timestamp logging // format: Optional string(s) to specify the timestamp format Timestamped(enable bool, format ...string) } // ClassType represents the type of a log entry. // It is an integer type used to categorize log entries (Text, JSON, Dump, Special, Raw), // influencing how handlers process and format them. type ClassType int // String converts a ClassType to its string representation. // It maps each class constant to a human-readable string, returning "UNKNOWN" for invalid classes. // Used by handlers to indicate the entry type in output (e.g., JSON fields). // Example: // // var class lx.ClassType = lx.ClassText // fmt.Println(class.String()) // Output: TEST func (t ClassType) String() string { switch t { case ClassText: return "TEST" // Note: Likely a typo, should be "TEXT" case ClassJSON: return "JSON" case ClassDump: return "DUMP" case ClassSpecial: return "SPECIAL" case ClassRaw: return "RAW" default: return "UNKNOWN" } } golang-github-olekukonko-ll-0.0.9/lx/ns.go000066400000000000000000000062331504320775200204360ustar00rootroot00000000000000package lx import ( "strings" "sync" ) // namespaceRule stores the cached result of Enabled. type namespaceRule struct { isEnabledByRule bool isDisabledByRule bool } // Namespace manages thread-safe namespace enable/disable states with caching. // The store holds explicit user-defined rules (path -> bool). // The cache holds computed effective states for paths (path -> namespaceRule) // based on hierarchical rules to optimize lookups. type Namespace struct { store sync.Map // path (string) -> rule (bool: true=enable, false=disable) cache sync.Map // path (string) -> namespaceRule } // Set defines an explicit enable/disable rule for a namespace path. // It clears the cache to ensure subsequent lookups reflect the change. func (ns *Namespace) Set(path string, enabled bool) { ns.store.Store(path, enabled) ns.clearCache() } // Load retrieves an explicit rule from the store for a path. // Returns the rule (true=enable, false=disable) and whether it exists. // Does not consider hierarchy or caching. func (ns *Namespace) Load(path string) (rule interface{}, found bool) { return ns.store.Load(path) } // Store directly sets a rule in the store, bypassing cache invalidation. // Intended for internal use or sync.Map parity; prefer Set for standard use. func (ns *Namespace) Store(path string, rule bool) { ns.store.Store(path, rule) } // clearCache clears the cache of Enabled results. // Called by Set to ensure consistency after rule changes. func (ns *Namespace) clearCache() { ns.cache.Range(func(key, _ interface{}) bool { ns.cache.Delete(key) return true }) } // Enabled checks if a path is enabled by namespace rules, considering the most // specific rule (path or closest prefix) in the store. Results are cached. // Args: // - path: Absolute namespace path to check. // - separator: Character delimiting path segments (e.g., "/", "."). // // Returns: // - isEnabledByRule: True if an explicit rule enables the path. // - isDisabledByRule: True if an explicit rule disables the path. // // If both are false, no explicit rule applies to the path or its prefixes. func (ns *Namespace) Enabled(path string, separator string) (isEnabledByRule bool, isDisabledByRule bool) { if path == "" { // Root path has no explicit rule return false, false } // Check cache if cachedValue, found := ns.cache.Load(path); found { if state, ok := cachedValue.(namespaceRule); ok { return state.isEnabledByRule, state.isDisabledByRule } ns.cache.Delete(path) // Remove invalid cache entry } // Compute: Most specific rule wins parts := strings.Split(path, separator) computedIsEnabled := false computedIsDisabled := false for i := len(parts); i >= 1; i-- { currentPrefix := strings.Join(parts[:i], separator) if val, ok := ns.store.Load(currentPrefix); ok { if rule := val.(bool); rule { computedIsEnabled = true computedIsDisabled = false } else { computedIsEnabled = false computedIsDisabled = true } break } } // Cache result, including (false, false) for no rule ns.cache.Store(path, namespaceRule{ isEnabledByRule: computedIsEnabled, isDisabledByRule: computedIsDisabled, }) return computedIsEnabled, computedIsDisabled } golang-github-olekukonko-ll-0.0.9/middleware.go000066400000000000000000000107201504320775200215040ustar00rootroot00000000000000package ll import ( "github.com/olekukonko/ll/lx" ) // Middleware represents a registered middleware and its operations in the logging pipeline. // It holds an ID for identification, a reference to the parent logger, and the handler function // that processes log entries. Middleware is used to transform or filter log entries before they // are passed to the logger's output handler. type Middleware struct { id int // Unique identifier for the middleware logger *Logger // Parent logger instance for context and logging operations fn lx.Handler // Handler function that processes log entries } // Remove unregisters the middleware from the logger’s middleware chain. // It safely removes the middleware by its ID, ensuring thread-safety with a mutex lock. // If the middleware or logger is nil, it returns early to prevent panics. // Example usage: // // // Using a named middleware function // mw := logger.Use(authMiddleware) // defer mw.Remove() // // // Using an inline middleware // mw = logger.Use(ll.Middle(func(e *lx.Entry) error { // if e.Level < lx.LevelWarn { // return fmt.Errorf("level too low") // } // return nil // })) // defer mw.Remove() func (m *Middleware) Remove() { // Check for nil middleware or logger to avoid panics if m == nil || m.logger == nil { return } // Acquire write lock to modify middleware slice m.logger.mu.Lock() defer m.logger.mu.Unlock() // Iterate through middleware slice to find and remove matching ID for i, entry := range m.logger.middleware { if entry.id == m.id { // Remove middleware by slicing out the matching entry m.logger.middleware = append(m.logger.middleware[:i], m.logger.middleware[i+1:]...) return } } } // Logger returns the parent logger for optional chaining. // This allows middleware to access the logger for additional operations, such as logging errors // or creating derived loggers. It is useful for fluent API patterns. // Example: // // mw := logger.Use(authMiddleware) // mw.Logger().Info("Middleware registered") func (m *Middleware) Logger() *Logger { return m.logger } // Error logs an error message at the Error level if the middleware blocks a log entry. // It uses the parent logger to emit the error and returns the middleware for chaining. // This is useful for debugging or auditing when middleware rejects a log. // Example: // // mw := logger.Use(ll.Middle(func(e *lx.Entry) error { // if e.Level < lx.LevelWarn { // return fmt.Errorf("level too low") // } // return nil // })) // mw.Error("Rejected low-level log") func (m *Middleware) Error(args ...any) *Middleware { m.logger.Error(args...) return m } // Errorf logs an error message at the Error level if the middleware blocks a log entry. // It uses the parent logger to emit the error and returns the middleware for chaining. // This is useful for debugging or auditing when middleware rejects a log. // Example: // // mw := logger.Use(ll.Middle(func(e *lx.Entry) error { // if e.Level < lx.LevelWarn { // return fmt.Errorf("level too low") // } // return nil // })) // mw.Errorf("Rejected low-level log") func (m *Middleware) Errorf(format string, args ...any) *Middleware { m.logger.Errorf(format, args...) return m } // middlewareFunc is a function adapter that implements the lx.Handler interface. // It allows plain functions with the signature `func(*lx.Entry) error` to be used as middleware. // The function should return nil to allow the log to proceed or a non-nil error to reject it, // stopping the log from being emitted by the logger. type middlewareFunc func(*lx.Entry) error // Handle implements the lx.Handler interface for middlewareFunc. // It calls the underlying function with the log entry and returns its result. // This enables seamless integration of function-based middleware into the logging pipeline. func (mf middlewareFunc) Handle(e *lx.Entry) error { return mf(e) } // Middle creates a middleware handler from a function. // It wraps a function with the signature `func(*lx.Entry) error` into a middlewareFunc, // allowing it to be used in the logger’s middleware pipeline. A non-nil error returned by // the function will stop the log from being emitted, ensuring precise control over logging. // Example: // // logger.Use(ll.Middle(func(e *lx.Entry) error { // if e.Level == lx.LevelDebug { // return fmt.Errorf("debug logs disabled") // } // return nil // })) func Middle(fn func(*lx.Entry) error) lx.Handler { return middlewareFunc(fn) } golang-github-olekukonko-ll-0.0.9/tests/000077500000000000000000000000001504320775200202025ustar00rootroot00000000000000golang-github-olekukonko-ll-0.0.9/tests/bench_test.go000066400000000000000000000052541504320775200226550ustar00rootroot00000000000000package tests import ( "fmt" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "io" "testing" ) // BenchmarkNamespaceLoop measures the performance of calling Namespace in a loop. // Each iteration creates a new child logger with a unique namespace and logs a message. func BenchmarkNamespaceLoop(b *testing.B) { logger := ll.New("app") logger.Handler(lh.NewTextHandler(io.Discard)) logger.Enable() b.ResetTimer() for i := 0; i < b.N; i++ { child := logger.Namespace(fmt.Sprintf("child%d", i)) child.Infof("Message") } } // BenchmarkNamespaceCached measures the performance of caching Namespace calls outside a loop. // Loggers are pre-created, and only logging is performed in the loop. func BenchmarkNamespaceCached(b *testing.B) { logger := ll.New("app") logger.Handler(lh.NewTextHandler(io.Discard)) logger.Enable() childLoggers := make([]*ll.Logger, b.N) for i := 0; i < b.N; i++ { childLoggers[i] = logger.Namespace(fmt.Sprintf("child%d", i)) } b.ResetTimer() for i := 0; i < b.N; i++ { childLoggers[i].Infof("Message") } } // BenchmarkCloneLoop measures the performance of calling Clone in a loop. // Each iteration creates a new logger with the same namespace and logs a message. func BenchmarkCloneLoop(b *testing.B) { logger := ll.New("app") logger.Handler(lh.NewTextHandler(io.Discard)) logger.Enable() b.ResetTimer() for i := 0; i < b.N; i++ { clone := logger.Clone() clone.Infof("Message") } } // BenchmarkFieldsLoggerLoop measures the performance of calling Fields followed by Logger in a loop. // Each iteration creates a new logger with embedded fields and logs a message. func BenchmarkFieldsLoggerLoop(b *testing.B) { logger := ll.New("app") logger.Handler(lh.NewTextHandler(io.Discard)) logger.Enable() b.ResetTimer() for i := 0; i < b.N; i++ { newLogger := logger.Fields("iteration", i).Logger() newLogger.Infof("Message") } } // BenchmarkPrefixLoop measures the performance of setting a prefix in a loop. // Each iteration modifies the logger’s prefix and logs a message. func BenchmarkPrefixLoop(b *testing.B) { logger := ll.New("app") logger.Handler(lh.NewTextHandler(io.Discard)) logger.Enable() b.ResetTimer() for i := 0; i < b.N; i++ { logger.Prefix(fmt.Sprintf("prefix%d: ", i)) logger.Infof("Message") } } // BenchmarkIndentLoop measures the performance of setting indentation in a loop. // Each iteration modifies the logger’s indentation level and logs a message. func BenchmarkIndentLoop(b *testing.B) { logger := ll.New("app") logger.Handler(lh.NewTextHandler(io.Discard)) logger.Enable() b.ResetTimer() for i := 0; i < b.N; i++ { logger.Indent(i % 5) // Vary indentation up to 5 levels logger.Infof("Message") } } golang-github-olekukonko-ll-0.0.9/tests/buffer_test.go000066400000000000000000000367301504320775200230520ustar00rootroot00000000000000package tests import ( "bytes" "encoding/json" "errors" "fmt" "io" "os" "runtime" "strings" "sync" "testing" "time" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" ) // errorWriter is an io.Writer that always returns an error. // It simulates a failing writer for testing error handling in log handlers. // Used in tests like ErrorLogging to verify error logging behavior. type errorWriter struct { err error // Error to return on Write } // Write implements io.Writer, always returning the configured error. // It returns 0 bytes written and the specified error, simulating a write failure. // Example (internal usage): // // w := &errorWriter{err: errors.New("write error")} // n, err := w.Write([]byte("test")) // Returns n=0, err="write error" func (w *errorWriter) Write(p []byte) (n int, err error) { return 0, w.err } // TestBufferedHandler tests the functionality of the Buffered handler. // It includes subtests for basic buffering, periodic flushing, overflow handling, // explicit flushing, shutdown behavior, concurrent access, error handling, and finalizer cleanup. // Each subtest verifies specific aspects of the Buffered handler's behavior under various conditions. func TestBufferedHandler(t *testing.T) { // Test basic buffering: flushing when batch size is reached t.Run("BasicFunctionality", func(t *testing.T) { buf := &bytes.Buffer{} // Buffer to capture output textHandler := lh.NewTextHandler(buf) // Create text handler handler := lh.NewBuffered(textHandler, // Create buffered handler lh.WithBatchSize(2), // Flush every 2 entries lh.WithFlushInterval(100*time.Millisecond)) // Flush every 100ms defer handler.Close() // Ensure cleanup // Send two log entries handler.Handle(&lx.Entry{Message: "test1"}) handler.Handle(&lx.Entry{Message: "test2"}) // Wait for batch flush time.Sleep(150 * time.Millisecond) // Allow time for flush to occur output := buf.String() // Get captured output // Verify both messages are present if !strings.Contains(output, "test1") || !strings.Contains(output, "test2") { t.Errorf("Expected both messages in output, got: %q", output) } }) // Test periodic flushing: flushing after a time interval t.Run("PeriodicFlushing", func(t *testing.T) { buf := &bytes.Buffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(100), // High batch size to avoid batch flush lh.WithFlushInterval(50*time.Millisecond)) // Flush every 50ms defer handler.Close() // Send one log entry handler.Handle(&lx.Entry{Message: "test"}) // Should flush after interval even though batch size not reached time.Sleep(75 * time.Millisecond) // Wait longer than flush interval // Verify message is flushed if !strings.Contains(buf.String(), "test") { t.Error("Expected message to be flushed after interval") } }) // Test overflow handling: behavior when buffer exceeds MaxBuffer t.Run("OverflowHandling", func(t *testing.T) { buf := &bytes.Buffer{} textHandler := lh.NewTextHandler(buf) var overflowCalled bool // Track overflow handler calls handler := lh.NewBuffered(textHandler, lh.WithBatchSize(2), // Flush every 2 entries lh.WithMaxBuffer(2), // Max buffer size of 2 lh.WithOverflowHandler(func(int) { overflowCalled = true }), // Set overflow callback ) defer handler.Close() // Fill buffer handler.Handle(&lx.Entry{Message: "test1"}) handler.Handle(&lx.Entry{Message: "test2"}) // This should trigger overflow err := handler.Handle(&lx.Entry{Message: "test3"}) // Verify overflow error if err == nil { t.Error("Expected error on overflow") } // Verify overflow handler was called if !overflowCalled { t.Error("Expected overflow handler to be called") } }) // Test explicit flush: flushing on demand with Flush() t.Run("ExplicitFlush", func(t *testing.T) { buf := &bytes.Buffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(100)) // High batch size defer handler.Close() // Send one log entry handler.Handle(&lx.Entry{Message: "test"}) handler.Flush() // Trigger immediate flush time.Sleep(10 * time.Millisecond) // Allow worker to process // Verify message is flushed if !strings.Contains(buf.String(), "test") { t.Error("Expected message to be flushed after explicit flush") } }) // Test shutdown: flushing remaining entries on Close() t.Run("ShutdownDrainsBuffer", func(t *testing.T) { buf := &bytes.Buffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(100)) // High batch size defer handler.Close() // Send one log entry handler.Handle(&lx.Entry{Message: "test"}) handler.Close() // Close should flush remaining entries // Verify message is flushed if !strings.Contains(buf.String(), "test") { t.Error("Expected message to be flushed on shutdown") } }) // Test concurrent access: handling multiple goroutines writing logs t.Run("ConcurrentAccess", func(t *testing.T) { buf := &bytes.Buffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(100), // High batch size lh.WithFlushInterval(10*time.Millisecond), // Frequent flushes lh.WithMaxBuffer(1000)) // Large buffer for concurrent writes defer handler.Close() var wg sync.WaitGroup // Synchronize goroutines // Send 100 log entries concurrently for i := 0; i < 100; i++ { wg.Add(1) go func(i int) { defer wg.Done() handler.Handle(&lx.Entry{Message: fmt.Sprintf("test%d", i)}) }(i) } wg.Wait() // Wait for all goroutines to finish handler.Flush() // Trigger final flush time.Sleep(50 * time.Millisecond) // Allow worker to process output := buf.String() t.Logf("Buffer output length: %d", len(output)) // Debug output // Verify all messages are present for i := 0; i < 100; i++ { if !strings.Contains(output, fmt.Sprintf("test%d", i)) { t.Errorf("Missing message test%d in output", i) } } }) // Test error handling: behavior with a failing writer t.Run("ErrorHandling", func(t *testing.T) { errWriter := &errorWriter{err: errors.New("write error")} // Failing writer textHandler := lh.NewTextHandler(errWriter) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(1)) // Flush every entry defer handler.Close() // Send one log entry err := handler.Handle(&lx.Entry{Message: "test"}) // Verify no error on Handle (buffered, not yet written) if err != nil { t.Errorf("Unexpected error on Handle: %v", err) } // Wait for flush to occur time.Sleep(50 * time.Millisecond) // Allow worker to attempt write }) // Test finalizer: flushing entries during garbage collection t.Run("Finalizer", func(t *testing.T) { buf := &bytes.Buffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(100)) // High batch size // Send one log entry handler.Handle(&lx.Entry{Message: "test"}) // Simulate GC (in real code this would happen automatically) runtime.SetFinalizer(handler, nil) // Remove the finalizer for test handler.Final() // Trigger finalizer behavior // Verify message is flushed if !strings.Contains(buf.String(), "test") { t.Error("Expected message to be flushed by finalizer") } }) } // TestBufferedHandlerOptions tests the configuration options of the Buffered handler. // It verifies default values and custom settings for BatchSize, FlushInterval, MaxBuffer, // and the overflow handler, ensuring proper initialization and behavior. func TestBufferedHandlerOptions(t *testing.T) { // Test default configuration values t.Run("DefaultValues", func(t *testing.T) { textHandler := lh.NewTextHandler(&bytes.Buffer{}) handler := lh.NewBuffered(textHandler) // Create with defaults // Verify default BatchSize if handler.Config().BatchSize != 100 { t.Errorf("Expected default BatchSize=100, got %d", handler.Config().BatchSize) } // Verify default FlushInterval if handler.Config().FlushInterval != 10*time.Second { t.Errorf("Expected default FlushInterval=10s, got %v", handler.Config().FlushInterval) } // Verify default MaxBuffer if handler.Config().MaxBuffer != 1000 { t.Errorf("Expected default MaxBuffer=1000, got %d", handler.Config().MaxBuffer) } }) // Test custom configuration options t.Run("CustomOptions", func(t *testing.T) { textHandler := lh.NewTextHandler(&bytes.Buffer{}) var called bool // Track overflow handler calls handler := lh.NewBuffered(textHandler, lh.WithBatchSize(50), // Custom batch size lh.WithFlushInterval(5*time.Second), // Custom flush interval lh.WithMaxBuffer(500), // Custom max buffer lh.WithOverflowHandler(func(int) { called = true }), // Custom overflow handler ) // Verify custom BatchSize if handler.Config().BatchSize != 50 { t.Errorf("Expected BatchSize=50, got %d", handler.Config().BatchSize) } // Verify custom FlushInterval if handler.Config().FlushInterval != 5*time.Second { t.Errorf("Expected FlushInterval=5s, got %v", handler.Config().FlushInterval) } // Verify custom MaxBuffer if handler.Config().MaxBuffer != 500 { t.Errorf("Expected MaxBuffer=500, got %d", handler.Config().MaxBuffer) } // Test overflow handler handler.Config().OnOverflow(1) // Trigger overflow callback // Verify overflow handler was called if !called { t.Error("Expected overflow handler to be called") } }) } // TestBufferedHandlerEdgeCases tests edge cases for Buffered handler configuration. // It verifies that invalid or extreme configuration values (e.g., zero batch size, // negative flush interval, small max buffer) are adjusted to sane defaults. func TestBufferedHandlerEdgeCases(t *testing.T) { // Test zero batch size adjustment t.Run("ZeroBatchSize", func(t *testing.T) { textHandler := lh.NewTextHandler(&bytes.Buffer{}) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(0)) // Invalid batch size // Verify adjustment to minimum if handler.Config().BatchSize != 1 { t.Errorf("Expected BatchSize to be adjusted to 1, got %d", handler.Config().BatchSize) } }) // Test negative flush interval adjustment t.Run("NegativeFlushInterval", func(t *testing.T) { textHandler := lh.NewTextHandler(&bytes.Buffer{}) handler := lh.NewBuffered(textHandler, lh.WithFlushInterval(-1*time.Second)) // Invalid interval // Verify adjustment to default if handler.Config().FlushInterval != 10*time.Second { t.Errorf("Expected FlushInterval to be adjusted to 10s, got %v", handler.Config().FlushInterval) } }) // Test small max buffer adjustment t.Run("SmallMaxBuffer", func(t *testing.T) { textHandler := lh.NewTextHandler(&bytes.Buffer{}) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(10), // Valid batch size lh.WithMaxBuffer(5), // Invalid max buffer (less than batch size) ) // Verify adjustment to ensure MaxBuffer >= BatchSize if handler.Config().MaxBuffer < handler.Config().BatchSize { t.Errorf("Expected MaxBuffer >= BatchSize, got %d < %d", handler.Config().MaxBuffer, handler.Config().BatchSize) } }) } // TestBufferedHandlerIntegration tests the Buffered handler's integration with various handlers. // It verifies that the Buffered handler works correctly with TextHandler, JSONHandler, // MultiHandler, and error-prone writers, ensuring proper flushing and error logging. func TestBufferedHandlerIntegration(t *testing.T) { // Test integration with TextHandler t.Run("WithTextHandler", func(t *testing.T) { buf := &bytes.Buffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(2), // Flush every 2 entries lh.WithFlushInterval(50*time.Millisecond)) // Flush every 50ms defer handler.Close() // Send two log entries handler.Handle(&lx.Entry{Message: "message1"}) handler.Handle(&lx.Entry{Message: "message2"}) // Wait for flush time.Sleep(75 * time.Millisecond) // Allow time for flush output := buf.String() // Verify both messages are present if !strings.Contains(output, "message1") || !strings.Contains(output, "message2") { t.Errorf("Expected both messages in output, got: %q", output) } }) // Test integration with JSONHandler t.Run("WithJSONHandler", func(t *testing.T) { buf := &bytes.Buffer{} jsonHandler := lh.NewJSONHandler(buf) handler := lh.NewBuffered(jsonHandler, lh.WithBatchSize(2)) // Flush every 2 entries defer handler.Close() // Send two log entries handler.Handle(&lx.Entry{Message: "message1"}) handler.Handle(&lx.Entry{Message: "message2"}) handler.Flush() // Trigger flush time.Sleep(50 * time.Millisecond) // Allow worker to process var count int // Count JSON entries // Decode JSON output dec := json.NewDecoder(buf) for { var entry map[string]interface{} if err := dec.Decode(&entry); err == io.EOF { break } else if err != nil { t.Fatal(err) } count++ } t.Logf("JSON entry count: %d", count) // Debug output // Verify two JSON entries if count != 2 { t.Errorf("Expected 2 JSON entries, got %d", count) } }) // Test integration with MultiHandler t.Run("WithMultiHandler", func(t *testing.T) { buf1 := &bytes.Buffer{} // Buffer for text output buf2 := &bytes.Buffer{} // Buffer for JSON output multiHandler := lh.NewMultiHandler( lh.NewTextHandler(buf1), // Text handler lh.NewJSONHandler(buf2), // JSON handler ) handler := lh.NewBuffered(multiHandler, lh.WithBatchSize(3)) // Flush every 3 entries defer handler.Close() // Send three log entries handler.Handle(&lx.Entry{Message: "test"}) handler.Handle(&lx.Entry{Message: "test"}) handler.Handle(&lx.Entry{Message: "test"}) handler.Flush() // Trigger flush time.Sleep(10 * time.Millisecond) // Allow worker to process // Verify text output textOutput := buf1.String() t.Logf("Text output: %q", textOutput) // Debug output if strings.Count(textOutput, "test") != 3 { t.Error("Expected 3 messages in text output") } // Verify JSON output var count int dec := json.NewDecoder(buf2) for dec.More() { var entry map[string]interface{} if err := dec.Decode(&entry); err != nil { t.Fatal(err) } count++ } t.Logf("JSON entry count: %d", count) // Debug output // Verify three JSON entries if count != 3 { t.Errorf("Expected 3 JSON entries, got %d", count) } }) // Test error logging with a failing writer t.Run("ErrorLogging", func(t *testing.T) { errWriter := &errorWriter{err: errors.New("write error")} // Failing writer textHandler := lh.NewTextHandler(errWriter) // Set up stderr capture before creating the handler oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stderr = w handler := lh.NewBuffered(textHandler) var errOutput bytes.Buffer errChan := make(chan struct{}) // Capture stderr in a goroutine go func() { defer close(errChan) io.Copy(&errOutput, r) }() // Send one log entry handler.Handle(&lx.Entry{Message: "message"}) handler.Flush() // Give time for the flush to occur time.Sleep(50 * time.Millisecond) // Clean up handler.Close() w.Close() os.Stderr = oldStderr <-errChan // Wait for stderr capture to complete // Verify error is logged to stderr if !strings.Contains(errOutput.String(), "write error") { t.Errorf("Expected error to be logged to stderr, got: %q", errOutput.String()) } }) } golang-github-olekukonko-ll-0.0.9/tests/handler_test.go000066400000000000000000000070261504320775200232120ustar00rootroot00000000000000package tests import ( "bytes" "encoding/json" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" "log/slog" "strings" "testing" ) // TestHandlers verifies the behavior of all log handlers (Text, Colorized, JSON, Slog, Multi). func TestHandlers(t *testing.T) { // Test TextHandler for plain text output t.Run("TextHandler", func(t *testing.T) { buf := &bytes.Buffer{} logger := ll.New("test").Enable().Handler(lh.NewTextHandler(buf)) logger.Fields("key", "value").Infof("Test text") if !strings.Contains(buf.String(), "[test] INFO: Test text [key=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Test text [key=value]") } }) // Test ColorizedHandler for ANSI-colored output t.Run("ColorizedHandler", func(t *testing.T) { buf := &bytes.Buffer{} logger := ll.New("test").Enable().Handler(lh.NewColorizedHandler(buf)) logger.Fields("key", "value").Infof("Test color") // Check for namespace presence, ignoring ANSI codes if !strings.Contains(buf.String(), "[test]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Test color [key=value]") } }) // Test JSONHandler for structured JSON output t.Run("JSONHandler", func(t *testing.T) { buf := &bytes.Buffer{} logger := ll.New("test").Enable().Handler(lh.NewJSONHandler(buf)) logger.Fields("key", "value").Infof("Test JSON") // Parse JSON output and verify fields var data lh.JsonOutput if err := json.Unmarshal(buf.Bytes(), &data); err != nil { t.Errorf("Expected no error, got %v", err) } if data.Level != lx.LevelInfo.String() { t.Errorf("Expected level=%q, got %q", "INFO", data.Level) } if data.Msg != "Test JSON" { t.Errorf("Expected message=%q, got %q", "Test JSON", data.Msg) } if data.Namespace != "test" { t.Errorf("Expected namespace=%q, got %q", "test", data.Namespace) } f := data.Fields if f["key"] != "value" { t.Errorf("Expected key=%q, got %q", "value", f["key"]) } }) // Test SlogHandler for compatibility with slog t.Run("SlogHandler", func(t *testing.T) { buf := &bytes.Buffer{} logger := ll.New("test").Enable().Handler(lh.NewSlogHandler(slog.NewTextHandler(buf, nil))) logger.Fields("key", "value").Infof("Test slog") output := buf.String() if !strings.Contains(output, "level=INFO") { t.Errorf("Expected %q to contain %q", output, "level=INFO") } if !strings.Contains(output, "msg=\"Test slog\"") { t.Errorf("Expected %q to contain %q", output, "msg=\"Test slog\"") } if !strings.Contains(output, "namespace=test") { t.Errorf("Expected %q to contain %q", output, "namespace=test") } if !strings.Contains(output, "key=value") { t.Errorf("Expected %q to contain %q", output, "key=value") } }) // Test MultiHandler for combining multiple handlers t.Run("MultiHandler", func(t *testing.T) { buf1 := &bytes.Buffer{} buf2 := &bytes.Buffer{} logger := ll.New("test").Enable().Handler(lh.NewMultiHandler( lh.NewTextHandler(buf1), lh.NewJSONHandler(buf2), )) logger.Fields("key", "value").Infof("Test multi") // Verify TextHandler output if !strings.Contains(buf1.String(), "[test] INFO: Test multi [key=value]") { t.Errorf("Expected %q to contain %q", buf1.String(), "[test] INFO: Test multi [key=value]") } // Verify JSONHandler output var data map[string]interface{} if err := json.Unmarshal(buf2.Bytes(), &data); err != nil { t.Errorf("Expected no error, got %v", err) } if data["msg"] != "Test multi" { t.Errorf("Expected message=%q, got %q", "Test multi", data["msg"]) } }) } golang-github-olekukonko-ll-0.0.9/tests/ll_ns_test.go000066400000000000000000000203431504320775200227010ustar00rootroot00000000000000// ll_ns_test.go package tests import ( "bytes" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" "strings" "testing" ) // TestNamespaceEnableWithCustomSeparator verifies that enabling a namespace with a custom separator // enables logging for that namespace and its descendants, even if the logger is initially disabled. func TestNamespaceEnableWithCustomSeparator(t *testing.T) { buf := &bytes.Buffer{} logger := ll.New("base").Disable().Separator(lx.Dot).Handler(lh.NewTextHandler(buf)) // Child loggers inherit disabled state a := logger.Namespace("a").Handler(lh.NewTextHandler(buf)) // base.a b := logger.Namespace("b").Handler(lh.NewTextHandler(buf)) // base.b c := logger.Namespace("c.1").Handler(lh.NewTextHandler(buf)) // base.c.1 d := logger.Namespace("c.1.2.4").Handler(lh.NewTextHandler(buf)) // base.c.1.2.4 // Enable "c.1" sub-namespace (full path: base.c.1) logger.NamespaceEnable("c.1") // Verify namespace enabled state if !logger.NamespaceEnabled("c.1") { t.Errorf("Expected namespace 'c.1' (base.c.1) to be enabled") } if !c.NamespaceEnabled("") { // Checks base.c.1 t.Errorf("Expected logger 'c' (base.c.1) to be enabled") } // Test logging buf.Reset() a.Infof("hello a from custom sep") b.Infof("hello b from custom sep") c.Infof("hello c from custom sep") d.Infof("hello d from custom sep") output := buf.String() // Expected logs expectedLogs := []string{ "[base.c.1] INFO: hello c from custom sep", "[base.c.1.2.4] INFO: hello d from custom sep", } for _, expected := range expectedLogs { if !strings.Contains(output, expected) { t.Errorf("Expected output to contain %q, got %q", expected, output) } } // Unexpected logs unexpectedLogs := []string{ "hello a from custom sep", "hello b from custom sep", } for _, unexpected := range unexpectedLogs { if strings.Contains(output, unexpected) { t.Errorf("Unexpected log %q in output: %q", unexpected, output) } } } // TestNamespaces verifies namespace creation, logging styles, and enable/disable behavior. func TestNamespaces(t *testing.T) { buf := &bytes.Buffer{} logger := ll.New("parent").Enable().Handler(lh.NewTextHandler(buf)) // Default "/" separator // Child logger inherits enabled state child := logger.Namespace("child").Handler(lh.NewTextHandler(buf)) // Test flat path logging child.Style(lx.FlatPath) buf.Reset() child.Infof("Child log") expectedFlatLog := "[parent/child] INFO: Child log" if !strings.Contains(buf.String(), expectedFlatLog) { t.Errorf("Expected %q, got %q", expectedFlatLog, buf.String()) } // Test nested path logging logger.Style(lx.NestedPath) child.Style(lx.NestedPath) buf.Reset() child.Infof("Nested log") expectedNestedLog := expectedNestedLogPrefix(child, lx.Arrow) + "INFO: Nested log" if !strings.Contains(buf.String(), expectedNestedLog) { t.Errorf("Expected %q, got %q", expectedNestedLog, buf.String()) } // Test NamespaceDisable logger.NamespaceDisable("child") // Disables parent/child if logger.NamespaceEnabled("child") { t.Errorf("Expected namespace 'child' (parent/child) to be disabled") } if child.NamespaceEnabled("") { t.Errorf("Expected namespace %q to be disabled", child.GetPath()) } buf.Reset() child.Infof("Should not log this") if buf.String() != "" { t.Errorf("Expected empty output, got %q", buf.String()) } // Test NamespaceEnable logger.NamespaceEnable("child") // Re-enables parent/child if !logger.NamespaceEnabled("child") { t.Errorf("Expected namespace 'child' (parent/child) to be enabled") } if !child.NamespaceEnabled("") { t.Errorf("Expected namespace %q to be enabled", child.GetPath()) } buf.Reset() child.Infof("Should log this again") expectedReEnabledLog := expectedNestedLogPrefix(child, lx.Arrow) + "INFO: Should log this again" if !strings.Contains(buf.String(), expectedReEnabledLog) { t.Errorf("Expected %q, got %q", expectedReEnabledLog, buf.String()) } } // expectedNestedLogPrefix generates the expected log prefix for nested path style. func expectedNestedLogPrefix(l *ll.Logger, arrow string) string { separator := l.GetSeparator() if separator == "" { separator = lx.Slash } if l.GetPath() != "" { parts := strings.Split(l.GetPath(), separator) var builder strings.Builder for i, part := range parts { builder.WriteString(lx.LeftBracket) builder.WriteString(part) builder.WriteString(lx.RightBracket) if i < len(parts)-1 { builder.WriteString(arrow) } } builder.WriteString(lx.Colon) builder.WriteString(lx.Space) return builder.String() } return "" } // TestSharedNamespaces verifies that namespace state affects derived loggers. func TestSharedNamespaces(t *testing.T) { buf := &bytes.Buffer{} parent := ll.New("parent").Enable().Handler(lh.NewTextHandler(buf)) // Disable child namespace parent.NamespaceDisable("child") // Sets parent/child to false // Create child logger child := parent.Namespace("child").Handler(lh.NewTextHandler(buf)).Style(lx.FlatPath) // Verify disabled state if parent.NamespaceEnabled("child") { t.Errorf("Expected namespace 'child' (parent/child) to be disabled") } if child.NamespaceEnabled("") { t.Errorf("Expected namespace %q to be disabled", child.GetPath()) } // Test logging (should be blocked) buf.Reset() child.Infof("Should not log from child") if buf.String() != "" { t.Errorf("Expected no output from %q, got %q", child.GetPath(), buf.String()) } // Enable child namespace parent.NamespaceEnable("child") // Sets parent/child to true if !parent.NamespaceEnabled("child") { t.Errorf("Expected namespace 'child' (parent/child) to be enabled") } if !child.NamespaceEnabled("") { t.Errorf("Expected namespace %q to be enabled", child.GetPath()) } // Test logging (should appear) buf.Reset() child.Infof("Should log from child") expectedLog := "[parent/child] INFO: Should log from child" if !strings.Contains(buf.String(), expectedLog) { t.Errorf("Expected %q, got %q", expectedLog, buf.String()) } } // TestNamespaceHierarchicalOverride verifies hierarchical namespace rules with overrides. func TestNamespaceHierarchicalOverride(t *testing.T) { l := ll.New("base").Disable() // Default "/" separator, instance disabled // Create buffers and loggers bufC1 := &bytes.Buffer{} bufC1D2 := &bytes.Buffer{} bufC1D2E3 := &bytes.Buffer{} bufC1D2E3F4 := &bytes.Buffer{} c1 := l.Namespace("c.1").Handler(lh.NewTextHandler(bufC1)) // base/c.1 c1d2 := l.Namespace("c.1/d.2").Handler(lh.NewTextHandler(bufC1D2)) // base/c.1/d.2 c1d2e3 := l.Namespace("c.1/d.2/e.3").Handler(lh.NewTextHandler(bufC1D2E3)) // base/c.1/d.2/e.3 c1d2e3f4 := l.Namespace("c.1/d.2/e.3/f.4").Handler(lh.NewTextHandler(bufC1D2E3F4)) // base/c.1/d.2/e.3/f.4 // Set namespace rules l.NamespaceDisable("c.1") // base/c.1 -> false l.NamespaceDisable("c.1/d.2") // base/c.1/d.2 -> false l.NamespaceEnable("c.1/d.2/e.3") // base/c.1/d.2/e.3 -> true // Verify namespace states if l.NamespaceEnabled("c.1") { t.Errorf("Expected namespace 'c.1' (base/c.1) to be disabled") } if l.NamespaceEnabled("c.1/d.2") { t.Errorf("Expected namespace 'c.1/d.2' (base/c.1/d.2) to be disabled") } if !l.NamespaceEnabled("c.1/d.2/e.3") { t.Errorf("Expected namespace 'c.1/d.2/e.3' (base/c.1/d.2/e.3) to be enabled") } if !l.NamespaceEnabled("c.1/d.2/e.3/f.4") { t.Errorf("Expected namespace 'c.1/d.2/e.3/f.4' (base/c.1/d.2/e.3/f.4) to be enabled") } if !c1d2e3f4.NamespaceEnabled("") { t.Errorf("Expected logger (base/c.1/d.2/e.3/f.4) to be enabled") } // Test logging c1.Infof("Log from c1") c1d2.Infof("Log from c1d2") c1d2e3.Infof("Log from c1d2e3") c1d2e3f4.Infof("Log from c1d2e3f4") // Verify outputs if strings.Contains(bufC1.String(), "Log from c1") { t.Errorf("Expected no log from c1 (base/c.1), got %q", bufC1.String()) } if strings.Contains(bufC1D2.String(), "Log from c1d2") { t.Errorf("Expected no log from c1d2 (base/c.1/d.2), got %q", bufC1D2.String()) } if !strings.Contains(bufC1D2E3.String(), "Log from c1d2e3") { t.Errorf("Expected log from c1d2e3 (base/c.1/d.2/e.3), got %q", bufC1D2E3.String()) } if !strings.Contains(bufC1D2E3F4.String(), "Log from c1d2e3f4") { t.Errorf("Expected log from c1d2e3f4 (base/c.1/d.2/e.3/f.4), got %q", bufC1D2E3F4.String()) } } golang-github-olekukonko-ll-0.0.9/tests/ll_test.go000066400000000000000000000463001504320775200222020ustar00rootroot00000000000000package tests import ( "bytes" "errors" "fmt" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lm" "github.com/olekukonko/ll/lx" "os" "strings" "testing" "time" ) // TestMain sets up the test environment and runs the test suite. // It resets the defaultLogger to a clean state to prevent state leakage between tests. func TestMain(m *testing.M) { // Run tests and exit with the appropriate status code os.Exit(m.Run()) } // TestLoggerConfiguration verifies the basic configuration methods of the Logger. func TestLoggerConfiguration(t *testing.T) { // Create a new logger with namespace "test" logger := ll.New("test").Enable() // Test Enable/Disable functionality logger = logger.Disable() logger.Infof("Should not log") // Should be ignored since logger is disabled if logger.Enabled() { t.Errorf("Expected enabled=false, got %v", logger.Enabled()) } logger = logger.Enable() if !logger.Enabled() { t.Errorf("Expected enabled=true, got %v", logger.Enabled()) } // Test Level functionality logger = logger.Level(lx.LevelWarn) if logger.GetLevel() != lx.LevelWarn { t.Errorf("Expected level=%v, got %v", lx.LevelWarn, logger.GetLevel()) } logger.Infof("Should not log") // Below Warn level, should be ignored logger.Warnf("Should log") // At Warn level, should be processed // Test Style functionality logger = logger.Style(lx.NestedPath) if logger.GetStyle() != lx.NestedPath { t.Errorf("Expected style=%v, got %v", lx.NestedPath, logger.GetStyle()) } logger = logger.Style(lx.FlatPath) if logger.GetStyle() != lx.FlatPath { t.Errorf("Expected style=%v, got %v", lx.FlatPath, logger.GetStyle()) } } // TestLoggingMethods verifies the core logging methods (Debug, Info, Warn, Error, Stack). func TestLoggingMethods(t *testing.T) { // Set up a logger with a buffer for capturing output buf := &bytes.Buffer{} logger := ll.New("test").Enable().Handler(lh.NewTextHandler(buf)).Level(lx.LevelDebug) // Test Debug logging buf.Reset() logger.Fields("key", "value").Debugf("Debug message") if !strings.Contains(buf.String(), "[test] DEBUG: Debug message [key=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] DEBUG: Debug message [key=value]") } // Test Info logging buf.Reset() logger.Fields("key", "value").Infof("Info message") if !strings.Contains(buf.String(), "[test] INFO: Info message [key=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Info message [key=value]") } // Test Warn logging buf.Reset() logger.Fields("key", "value").Warnf("Warn message") if !strings.Contains(buf.String(), "[test] WARN: Warn message [key=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] WARN: Warn message [key=value]") } // Test Error logging buf.Reset() logger.Fields("key", "value").Errorf("Error message") if !strings.Contains(buf.String(), "[test] ERROR: Error message [key=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] ERROR: Error message [key=value]") } // Test Stack logging with stack trace buf.Reset() logger.Fields("key", "value").Stackf("Error with stack") output := buf.String() if !strings.Contains(output, "[test] ERROR: Error with stack") { t.Errorf("Expected %q to contain %q", output, "[test] ERROR: Error with stack") } if !strings.Contains(output, "key=value") { t.Errorf("Expected %q to contain %q", output, "key=value") } if !strings.Contains(output, "stack") { t.Errorf("Expected %q to contain %q", output, "stack") } } // TestBuilderFields verifies the Fields and Field methods for adding metadata to logs. func TestBuilderFields(t *testing.T) { // Set up a logger with a buffer for capturing output buf := &bytes.Buffer{} logger := ll.New("test").Enable().Handler(lh.NewTextHandler(buf)) // Test variadic Fields with multiple key-value pairs buf.Reset() logger.Fields("k1", "v1", "k2", "v2", "k3", 123).Infof("Test variadic") if !strings.Contains(buf.String(), "[test] INFO: Test variadic [k1=v1 k2=v2 k3=123]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Test variadic [k1=v1 k2=v2 k3=123]") } // Test map-based Field with a pre-constructed map buf.Reset() fields := map[string]interface{}{"k1": "v1", "k2": "v2", "k3": 123} logger.Field(fields).Infof("Test map") if !strings.Contains(buf.String(), "[test] INFO: Test map [k1=v1 k2=v2 k3=123]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Test map [k1=v1 k2=v2 k3=123]") } // Test variadic Fields with uneven key-value pairs buf.Reset() logger.Fields("k1", "v1", "k2").Infof("Test uneven") if !strings.Contains(buf.String(), "[test] INFO: Test uneven [error=uneven key-value pairs in Fields: [k2] k1=v1]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Test uneven [error=uneven key-value pairs in Fields: [k2] k1=v1]") } // Test variadic Fields with a non-string key buf.Reset() logger.Fields("k1", "v1", 42, "v2").Infof("Test non-string") if !strings.Contains(buf.String(), "[test] INFO: Test non-string [error=non-string key in Fields: 42 k1=v1]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Test non-string [error=non-string key in Fields: 42 k1=v1]") } } // TestRateLimiting verifies rate-limiting functionality for a log level. func TestRateLimiting(t *testing.T) { // Set up a logger with a buffer for capturing output buf := &bytes.Buffer{} logger := ll.New("test").Enable().Handler(lh.NewTextHandler(buf)) logger.Use(lm.NewRateLimiter(lx.LevelInfo, 2, time.Second)) // Test logging within the rate limit (2 logs allowed) buf.Reset() logger.Infof("Log 1") logger.Infof("Log 2") lines := strings.Split(strings.TrimSpace(buf.String()), "\n") if len(lines) != 2 { t.Errorf("Expected %d logs, got %d", 2, len(lines)) } if !strings.Contains(buf.String(), "Log 1") { t.Errorf("Expected %q to contain %q", buf.String(), "Log 1") } if !strings.Contains(buf.String(), "Log 2") { t.Errorf("Expected %q to contain %q", buf.String(), "Log 2") } // Test exceeding the rate limit buf.Reset() logger.Infof("Log 3") // Should be blocked if buf.String() != "" { t.Errorf("Expected empty buffer, got %q", buf.String()) } // Test logging after the interval resets time.Sleep(time.Second) buf.Reset() logger.Infof("Log 4") if !strings.Contains(buf.String(), "Log 4") { t.Errorf("Expected %q to contain %q", buf.String(), "Log 4") } } // TestSampling verifies sampling functionality for a log level. func TestSampling(t *testing.T) { buf := &bytes.Buffer{} logger := ll.New("test").Enable().Handler(lh.NewTextHandler(buf)).Clear() // Clear middleware logger.Use(lm.NewSampling(lx.LevelInfo, 0.0)) // Never log // Test logging with 0.0 sampling rate buf.Reset() logger.Infof("Should not log") if buf.String() != "" { t.Errorf("Expected empty buffer, got %q", buf.String()) } // Test logging with 1.0 sampling rate logger = ll.New("test").Enable().Handler(lh.NewTextHandler(buf)).Clear() // Fresh logger logger.Use(lm.NewSampling(lx.LevelInfo, 1.0)) // Always log buf.Reset() logger.Infof("Should log") if !strings.Contains(buf.String(), "[test] INFO: Should log") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Should log") } } // TestConditionalLogging verifies conditional logging using the If method. func TestConditionalLogging(t *testing.T) { // Set up a logger with a buffer for capturing output buf := &bytes.Buffer{} logger := ll.New("VC").Enable().Handler(lh.NewTextHandler(buf)).Level(lx.LevelDebug) // Test false condition with variadic Fields buf.Reset() logger.If(false).Fields("key", "value").Infof("Should not log") if buf.String() != "" { t.Errorf("Expected empty buffer, got %q", buf.String()) } // Test true condition with variadic Fields buf.Reset() logger.If(true).Fields("key", "value").Infof("Should log") if !strings.Contains(buf.String(), "[VC] INFO: Should log [key=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[VC] INFO: Should log [key=value]") } // Test false condition with map-based Field buf.Reset() fields := map[string]interface{}{"key": "value"} logger.If(false).Field(fields).Infof("Should not log") if buf.String() != "" { t.Errorf("Expected empty buffer, got %q", buf.String()) } // Test true condition with map-based Field buf.Reset() logger.If(true).Field(fields).Infof("Should log") if !strings.Contains(buf.String(), "[VC] INFO: Should log [key=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[VC] INFO: Should log [key=value]") } // Test variadic Fields with uneven pairs under true condition buf.Reset() logger.If(true).Fields("key", "value", "odd").Infof("Test uneven") if !strings.Contains(buf.String(), "[VC] INFO: Test uneven [error=uneven key-value pairs in Fields: [odd] key=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[VC] INFO: Test uneven [error=uneven key-value pairs in Fields: [odd] key=value]") } // Test variadic Fields with non-string key under true condition buf.Reset() logger.If(true).Fields("key", "value", 42, "value2").Infof("Test non-string") if !strings.Contains(buf.String(), "[VC] INFO: Test non-string [error=non-string key in Fields: 42 key=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[VC] INFO: Test non-string [error=non-string key in Fields: 42 key=value]") } // Test Conditional Stack logging with stack trace t.Run("ConditionalStack", func(t *testing.T) { buf := &bytes.Buffer{} logger := ll.New("test/app").Enable().Style(lx.NestedPath).Handler(lh.NewTextHandler(buf)).Level(lx.LevelDebug).Prefix("ERR: ").Indent(1) logger = logger.Context(map[string]interface{}{"ctx": "value"}) logger.If(true).Stackf("error occurred: %v", "timeout") expectedStack := "[stack]" if !strings.Contains(buf.String(), expectedStack) { t.Errorf("Expected %q to contain %q; \ngot %q", buf.String(), expectedStack, buf.String()) } buf.Reset() logger.If(false).Stackf("should not log: %v", "timeout") if buf.String() != "" { t.Errorf("Expected empty buffer, got %q", buf.String()) } }) } // TestMiddleware verifies the Use method for adding middleware to process log entries. func TestMiddleware(t *testing.T) { // Set up a logger with a buffer for capturing output buf := &bytes.Buffer{} logger := ll.New("test").Enable().Handler(lh.NewTextHandler(buf)).Level(lx.LevelDebug) // Test middleware that adds a field logger = logger.Use(ll.Middle(func(e *lx.Entry) error { if e.Fields == nil { e.Fields = make(map[string]interface{}) } e.Fields["extra"] = "value" return nil })).Logger() buf.Reset() logger.Infof("Test with extra field") if !strings.Contains(buf.String(), "[test] INFO: Test with extra field [extra=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Test with extra field [extra=value]") } // Test middleware that filters logs by level logger = logger.Use(ll.Middle(func(e *lx.Entry) error { if e.Level >= lx.LevelWarn { return nil } return fmt.Errorf("level too low") })).Logger() buf.Reset() logger.Infof("Should not log") // Below Warn level, should be ignored if buf.String() != "" { t.Errorf("Expected empty buffer, got %q", buf.String()) } buf.Reset() logger.Warnf("Should log") if !strings.Contains(buf.String(), "[test] WARN: Should log [extra=value]") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] WARN: Should log [extra=value]") } // Test middleware that skips all logs logger = logger.Use(ll.Middle(func(e *lx.Entry) error { return fmt.Errorf("skip all") })).Logger() buf.Reset() logger.Warnf("Should not log") // Should be ignored by middleware if buf.String() != "" { t.Errorf("Expected empty buffer, got %q", buf.String()) } } // TestClone verifies the Clone method for creating a logger with the same namespace and isolated context. func TestClone(t *testing.T) { buf := &bytes.Buffer{} logger := ll.New("app").Enable().Handler(lh.NewTextHandler(buf)).Level(lx.LevelInfo).Style(lx.NestedPath) t.Logf("Initial logger enabled: %v, level: %v, style: %v", logger.Enabled(), logger.GetLevel(), logger.GetStyle()) t.Run("Namespace", func(t *testing.T) { clone := logger.Clone() if clone.GetPath() != "app" { t.Errorf("Expected clone namespace %q, got %q", "app", clone.GetPath()) } }) t.Run("Configuration", func(t *testing.T) { clone := logger.Clone() if !clone.Enabled() { t.Errorf("Expected clone enabled=true, got %v", clone.Enabled()) } if clone.GetLevel() != lx.LevelInfo { t.Errorf("Expected clone level=%v, got %v", lx.LevelInfo, clone.GetLevel()) } if clone.GetStyle() != lx.NestedPath { t.Errorf("Expected clone style=%v, got %v", lx.NestedPath, clone.GetStyle()) } }) t.Run("ContextIsolation", func(t *testing.T) { logger = logger.Context(map[string]interface{}{"parent": "value"}) t.Logf("Parent context: %v", logger.GetContext()) // Use GetContext clone := logger.Clone() buf.Reset() clone.Fields("clone", "value").Infof("Clone message") expected := "[app]" + lx.Colon + lx.Space + "INFO: Clone message [clone=value]" output := buf.String() t.Logf("Clone output: %q", output) if !strings.Contains(output, expected) { t.Errorf("Expected %q to contain %q; got %q", output, expected, output) } if strings.Contains(output, "parent=value") { t.Errorf("Expected %q not to contain %q", output, "parent=value") } }) t.Run("ParentContext", func(t *testing.T) { t.Logf("Parent context before logging: %v", logger.GetContext()) // Use GetContext t.Logf("Parent enabled: %v, system active: %v", logger.Enabled(), ll.Active()) buf.Reset() logger.Infof("Parent message") output := buf.String() expected := "[app]" + lx.Colon + lx.Space + "INFO: Parent message [parent=value]" t.Logf("Parent output: %q", output) if !strings.Contains(output, expected) { t.Errorf("Expected %q to contain %q; got %q", output, expected, output) } if strings.Contains(output, "clone=value") { t.Errorf("Expected %q not to contain %q", output, "clone=value") } }) } // TestPrefix verifies the Prefix method for prepending a string to log messages. func TestPrefix(t *testing.T) { // Set up a logger with a buffer for capturing output buf := &bytes.Buffer{} logger := ll.New("app").Enable().Handler(lh.NewTextHandler(buf)).Level(lx.LevelInfo) // Test setting a prefix t.Run("SetPrefix", func(t *testing.T) { buf.Reset() logger = logger.Prefix("INFO: ") logger.Infof("Test message") output := buf.String() //t.Logf("Buffer output: %q", output) if !strings.Contains(output, "INFO: Test message") { t.Errorf("Expected %q to contain %q", output, "INFO: Test message") } }) // Test updating the prefix t.Run("UpdatePrefix", func(t *testing.T) { buf.Reset() logger = logger.Prefix("DEBUG: ") logger.Infof("Another message") output := buf.String() if !strings.Contains(output, "DEBUG: Another message") { t.Errorf("Expected %q to contain %q", output, "DEBUG: Another message") } }) // Test removing the prefix t.Run("RemovePrefix", func(t *testing.T) { buf.Reset() logger = logger.Prefix("") logger.Infof("No prefix") output := buf.String() if !strings.Contains(output, "INFO: No prefix") { t.Errorf("Expected message without prefix, got: %q", output) } }) } // TestIndent verifies the Indent method for adding double spaces to log messages. func TestIndent(t *testing.T) { // Set up a logger with a buffer for capturing output buf := &bytes.Buffer{} logger := ll.New("app").Enable().Handler(lh.NewTextHandler(buf)).Level(lx.LevelInfo) // Test setting indentation to 2 (4 spaces) t.Run("SetIndent", func(t *testing.T) { logger = logger.Indent(2) buf.Reset() logger.Infof("Test message") output := buf.String() t.Logf("Buffer output: %q", output) // Debug output if !strings.Contains(output, "[app] INFO: Test message") { t.Errorf("Expected %q to contain %q", output, "[app] INFO: Test message") } }) // Test updating indentation to 1 (2 spaces) t.Run("UpdateIndent", func(t *testing.T) { logger = logger.Indent(1) buf.Reset() logger.Infof("Another message") if !strings.Contains(buf.String(), "[app] INFO: Another message") { t.Errorf("Expected %q to contain %q", buf.String(), "[app] INFO: Another message") } }) // Test removing indentation t.Run("RemoveIndent", func(t *testing.T) { logger = logger.Indent(0) buf.Reset() logger.Infof("No indent") if !strings.Contains(buf.String(), "[app] INFO: No indent") { t.Errorf("Expected %q to contain %q", buf.String(), "[app] INFO: No indent") } }) } // failingWriter is a test writer that always fails to write, used to simulate handler errors. type failingWriter struct{} // Write implements io.Writer, always returning an error. func (w *failingWriter) Write(p []byte) (n int, err error) { return 0, errors.New("write failed") } // TestHandlerErrors verifies handler behavior when errors occur. func TestHandlerErrors(t *testing.T) { // Test single TextHandler buf := &bytes.Buffer{} logger := ll.New("test").Enable().Level(lx.LevelDebug).Handler(lh.NewTextHandler(buf)) logger.Infof("Test single handler") if !strings.Contains(buf.String(), "[test] INFO: Test single handler") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Test single handler") } // Test MultiHandler with a failing TextHandler buf.Reset() logger = logger.Handler(lh.NewMultiHandler( lh.NewTextHandler(buf), lh.NewTextHandler(&failingWriter{}), )) logger.Infof("Test multi error") if !strings.Contains(buf.String(), "[test] INFO: Test multi error") { t.Errorf("Expected %q to contain %q", buf.String(), "[test] INFO: Test multi error") } } // TestNamespaceToggle verifies the NamespaceEnable and NamespaceDisable methods. func TestNamespaceToggle(t *testing.T) { // Create a logger and test namespace toggling logger := ll.New("test").Disable() logger = logger.NamespaceEnable("parent/child") if !logger.NamespaceEnabled("parent/child") { t.Errorf("parent/child should be enabled") } logger = logger.NamespaceDisable("parent/child") if logger.Enabled() { t.Errorf("parent/child should be disabled") } } // TestTextHandler verifies the TextHandler’s output format. func TestTextHandler(t *testing.T) { // Create a buffer and TextHandler var buf bytes.Buffer h := lh.NewTextHandler(&buf) // Create a test log entry e := &lx.Entry{ Timestamp: time.Now(), Level: lx.LevelInfo, Message: "test", Namespace: "", Fields: map[string]interface{}{"key": 1}, } // Process the entry if err := h.Handle(e); err != nil { t.Errorf("Handle failed: %v", err) } // Verify the output format if !strings.Contains(buf.String(), "INFO: test [key=1]") { t.Errorf("Unexpected output: %s", buf.String()) } } func TestSates(t *testing.T) { t.Run("SuspendAndResume", func(t *testing.T) { buf := &bytes.Buffer{} logger := ll.New("app").Enable().Handler(lh.NewTextHandler(buf)) logger.Suspend() if !logger.Suspended() { t.Error("Expected logger to be suspended") } logger.Info("Ignored") // Should not appear logger.Resume() if logger.Suspended() { t.Error("Expected logger to be resumed") } logger.Info("Logged") output := buf.String() if strings.Contains(output, "Ignored") { t.Error("Expected 'Ignored' to be suppressed") } if !strings.Contains(output, "Logged") { t.Error("Expected 'Logged' in output") } }) }