pax_global_header00006660000000000000000000000064151615233730014520gustar00rootroot0000000000000052 comment=06e260f8da6dffec31a7a4a49bef5b34ac949cad golang-github-olekukonko-ll-0.1.8/000077500000000000000000000000001516152337300170415ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/.github/000077500000000000000000000000001516152337300204015ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/.github/workflows/000077500000000000000000000000001516152337300224365ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/.github/workflows/go.yml000066400000000000000000000014151516152337300235670ustar00rootroot00000000000000# This workflow will build a golang project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go name: Go on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: test-linux: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' cache: false - name: Test run: go test -v ./... test-windows: runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' cache: false - name: Test run: go test -v ./...golang-github-olekukonko-ll-0.1.8/.github/workflows/release.yml000066400000000000000000000012311516152337300245760ustar00rootroot00000000000000name: Release on: push: tags: - 'v*' permissions: contents: write jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Create Release uses: softprops/action-gh-release@v1 with: # files: | # csv2table-windows-amd64.exe # csv2table-windows-arm64.exe # csv2table-linux-amd64 # csv2table-linux-arm64 # csv2table-darwin-amd64 # csv2table-darwin-arm64 draft: false prerelease: false golang-github-olekukonko-ll-0.1.8/.gitignore000066400000000000000000000000311516152337300210230ustar00rootroot00000000000000.idea lab tmp #_* _test/ golang-github-olekukonko-ll-0.1.8/.goreleaser.yaml000066400000000000000000000012661516152337300221400ustar00rootroot00000000000000# yaml-language-server: $schema=https://goreleaser.com/static/schema.json version: 2 project_name: ll # For a library repo, publish source archives instead of binaries. source: enabled: true name_template: "{{ .ProjectName }}_{{ .Version }}" # Optional: include/exclude files in the source archive (defaults are usually fine) # files: # - README.md # - LICENSE # - go.mod # - go.sum # - "**/*.go" # No binaries to build. builds: [] ## Other Information checksum: name_template: "checksums.txt" snapshot: version_template: "{{ .Tag }}-next" changelog: sort: asc filters: exclude: - "^docs:" - "^test:" - "^chore:" - "^ci:" golang-github-olekukonko-ll-0.1.8/LICENSE000066400000000000000000000020541516152337300200470ustar00rootroot00000000000000MIT 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.1.8/Makefile000066400000000000000000000057141516152337300205100ustar00rootroot00000000000000# Git remote for pushing tags REMOTE ?= origin # Version for release tagging (required for tag/release targets) RELEASE_VERSION ?= # Convenience GO ?= go GOLANGCI ?= golangci-lint GORELEASER?= goreleaser .PHONY: help \ test race bench fmt tidy lint check \ ensure-clean ensure-release-version tag tag-delete \ release release-dry help: @echo "Targets:" @echo " fmt - gofmt + go fmt" @echo " tidy - go mod tidy" @echo " test - go test ./..." @echo " race - go test -race ./..." @echo " bench - go test -bench=. ./..." @echo " lint - golangci-lint run ./... (if installed)" @echo " check - fmt + tidy + test + race" @echo "" @echo "Release targets:" @echo " tag - Create annotated tag RELEASE_VERSION and push" @echo " tag-delete - Delete tag RELEASE_VERSION locally + remote" @echo " release - tag + goreleaser release --clean (if you use goreleaser)" @echo " release-dry - tag + goreleaser release --clean --skip=publish" @echo "" @echo "Usage:" @echo " make check" @echo " make tag RELEASE_VERSION=v0.1.2" @echo " make release RELEASE_VERSION=v0.1.2" fmt: @echo "Formatting..." gofmt -w -s . $(GO) fmt ./... tidy: @echo "Tidying..." $(GO) mod tidy test: @echo "Testing..." $(GO) test ./... -count=1 race: @echo "Race testing..." $(GO) test ./... -race -count=1 bench: @echo "Bench..." $(GO) test ./... -bench=. -run=^$$ lint: @echo "Linting..." @command -v $(GOLANGCI) >/dev/null 2>&1 || { echo "golangci-lint not found"; exit 1; } $(GOLANGCI) run ./... check: fmt tidy test race # -------------------------- # Release helpers # -------------------------- ensure-clean: @echo "Checking git working tree..." @git diff --quiet || (echo "Error: tracked changes exist. Commit/stash them."; exit 1) @test -z "$$(git status --porcelain)" || (echo "Error: uncommitted/untracked files:"; git status --porcelain; exit 1) @echo "OK: working tree clean" ensure-release-version: @test -n "$(RELEASE_VERSION)" || (echo "Error: set RELEASE_VERSION, e.g. make tag RELEASE_VERSION=v0.1.2"; exit 1) tag: ensure-clean ensure-release-version @if git rev-parse "$(RELEASE_VERSION)" >/dev/null 2>&1; then \ echo "Error: tag $(RELEASE_VERSION) already exists. Bump version."; \ exit 1; \ fi @echo "Tagging $(RELEASE_VERSION) at HEAD $$(git rev-parse --short HEAD)" @git tag -a $(RELEASE_VERSION) -m "$(RELEASE_VERSION)" @git push $(REMOTE) $(RELEASE_VERSION) tag-delete: ensure-release-version @echo "Deleting tag $(RELEASE_VERSION) locally + remote..." @git tag -d $(RELEASE_VERSION) 2>/dev/null || true @git push $(REMOTE) :refs/tags/$(RELEASE_VERSION) || true release: tag @command -v $(GORELEASER) >/dev/null 2>&1 || { echo "goreleaser not found"; exit 1; } $(GORELEASER) release --clean release-dry: tag @command -v $(GORELEASER) >/dev/null 2>&1 || { echo "goreleaser not found"; exit 1; } $(GORELEASER) release --clean --skip=publish golang-github-olekukonko-ll-0.1.8/README.md000066400000000000000000000275601516152337300203320ustar00rootroot00000000000000# 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, syslog, VictoriaLogs, and compatibility with Go's `slog`. It's ideal for applications requiring fine-grained log control, extensibility, and scalability. ## Key Features - **Logging Enabled by Default** - Zero configuration to start logging - **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 rate limiting, sampling, and deduplication - **Conditional & Error-Based Logging** - Optimize performance with fluent `If`, `IfErr`, `IfAny`, `IfOne` chains - **Multiple Output Formats** - Text, JSON, colorized ANSI, syslog, VictoriaLogs, and `slog` integration - **Advanced Debugging Utilities** - Source-aware `Dbg()`, hex/ASCII `Dump()`, private field `Inspect()`, and stack traces - **Production Ready** - Buffered batching, log rotation, duplicate suppression, and rate limiting - **Thread-Safe** - Built for high-concurrency with atomic operations, sharded mutexes, and lock-free fast paths - **Performance Optimized** - Zero allocations for disabled logs, sync.Pool buffers, LRU caching for source files ## Installation Install `ll` using Go modules: ```bash go get github.com/olekukonko/ll ``` Requires Go 1.21 or later. ## Quick Start ```go package main import "github.com/olekukonko/ll" func main() { // Logger is ENABLED by default - no .Enable() needed! logger := ll.New("app") // Basic logging - works immediately logger.Info("Server starting") // Output: [app] INFO: Server starting logger.Warn("Memory high") // Output: [app] WARN: Memory high logger.Error("Connection failed") // Output: [app] ERROR: Connection failed // Structured fields logger.Fields("user", "alice", "status", 200).Info("Login successful") // Output: [app] INFO: Login successful [user=alice status=200] } ``` **That's it. No `.Enable()`, no handlers to configure—it just works.** ## Core Concepts ### 1. Enabled by Default, Configurable When Needed Unlike many logging libraries that require explicit enabling, `ll` **logs immediately**. This eliminates boilerplate and reduces the chance of missing logs in production. ```go // This works out of the box: ll.Info("Service started") // Output: [] INFO: Service started // But you still have full control: ll.Disable() // Global shutdown ll.Enable() // Reactivate ``` ### 2. Hierarchical Namespaces Organize logs hierarchically with precise control over subsystems: ```go // Create a logger hierarchy root := ll.New("app") db := root.Namespace("database") cache := root.Namespace("cache").Style(lx.NestedPath) // Control logging per namespace root.NamespaceEnable("app/database") // Enable database logs root.NamespaceDisable("app/cache") // Disable cache logs db.Info("Connected") // Output: [app/database] INFO: Connected cache.Info("Hit") // No output (disabled) ``` ### 3. Structured Logging with Ordered Fields Fields maintain insertion order and support fluent chaining: ```go // Fluent key-value pairs logger. Fields("request_id", "req-123"). Fields("user", "alice"). Fields("duration_ms", 42). Info("Request processed") // Map-based fields logger.Field(map[string]interface{}{ "method": "POST", "path": "/api/users", }).Debug("API call") // Persistent context (included in ALL subsequent logs) logger.AddContext("environment", "production", "version", "1.2.3") logger.Info("Deployed") // Output: ... [environment=production version=1.2.3] ``` ### 4. Conditional & Error-Based Logging Optimize performance with fluent conditional chains that **completely skip processing** when conditions are false: ```go // Boolean conditions logger.If(debugMode).Debug("Detailed diagnostics") // No overhead when false logger.If(featureEnabled).Info("Feature used") // Error conditions err := db.Query() logger.IfErr(err).Error("Query failed") // Logs only if err != nil // Multiple conditions - ANY true logger.IfErrAny(err1, err2, err3).Fatal("System failure") // Multiple conditions - ALL true logger.IfErrOne(validateErr, authErr).Error("Both checks failed") // Chain conditions logger. If(debugMode). IfErr(queryErr). Fields("query", sql). Debug("Query debug") ``` **Performance**: When conditions are false, the logger returns immediately with zero allocations. ### 5. Powerful Debugging Toolkit `ll` includes advanced debugging utilities not found in standard logging libraries: #### Dbg() - Source-Aware Variable Inspection Captures both variable name AND value from your source code: ```go x := 42 user := &User{Name: "Alice"} ll.Dbg(x, user) // Output: [file.go:123] x = 42, *user = &{Name:Alice} ``` #### Dump() - Hex/ASCII Binary Inspection Perfect for protocol debugging and binary data: ```go ll.Handler(lh.NewColorizedHandler(os.Stdout)) ll.Dump([]byte("hello\nworld")) // Output: Colorized hex/ASCII dump with offset markers ``` #### Inspect() - Private Field Reflection Reveals unexported fields, embedded structs, and pointer internals: ```go type secret struct { password string // unexported! } s := secret{password: "hunter2"} ll.Inspect(s) // Output: [file.go:123] INSPECT: { // "(password)": "hunter2" // Note the parentheses // } ``` #### Stack() - Configurable Stack Traces ```go ll.StackSize(8192) // Larger buffer for deep stacks ll.Stack("Critical failure") // Output: ERROR: Critical failure [stack=goroutine 1 [running]...] ``` #### Mark() - Execution Flow Tracing ```go func process() { ll.Mark() // *MARK*: [file.go:123] ll.Mark("phase1") // *phase1*: [file.go:124] // ... work ... } ``` ### 6. Production-Ready Handlers ```go import ( "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/l3rd/syslog" "github.com/olekukonko/ll/l3rd/victoria" ) // JSON for structured logging logger.Handler(lh.NewJSONHandler(os.Stdout)) // Colorized for development logger.Handler(lh.NewColorizedHandler(os.Stdout, lh.WithColorTheme("dark"), lh.WithColorIntensity(lh.IntensityVibrant), )) // Buffered for high throughput (100 entries or 10 seconds) buffered := lh.NewBuffered( lh.NewJSONHandler(os.Stdout), lh.WithBatchSize(100), lh.WithFlushInterval(10 * time.Second), ) logger.Handler(buffered) defer buffered.Close() // Ensures flush on exit // Syslog integration syslogHandler, _ := syslog.New( syslog.WithTag("myapp"), syslog.WithFacility(syslog.LOG_LOCAL0), ) logger.Handler(syslogHandler) // VictoriaLogs (cloud-native) victoriaHandler, _ := victoria.New( victoria.WithURL("http://victoria-logs:9428"), victoria.WithAppName("payment-service"), victoria.WithEnvironment("production"), victoria.WithBatching(200, 5*time.Second), ) logger.Handler(victoriaHandler) ``` ### 7. Middleware Pipeline Transform, filter, or reject logs with a middleware pipeline: ```go // Rate limiting - 10 logs per second maximum rateLimiter := lm.NewRateLimiter(lx.LevelInfo, 10, time.Second) logger.Use(rateLimiter) // Sampling - 10% of debug logs sampler := lm.NewSampling(lx.LevelDebug, 0.1) logger.Use(sampler) // Deduplication - suppress identical logs for 2 seconds deduper := lh.NewDedup(logger.GetHandler(), 2*time.Second) logger.Handler(deduper) // Custom middleware logger.Use(ll.Middle(func(e *lx.Entry) error { if strings.Contains(e.Message, "password") { return fmt.Errorf("sensitive information redacted") } return nil })) ``` ### 8. Global Convenience API Use package-level functions for quick logging without creating loggers: ```go import "github.com/olekukonko/ll" func main() { ll.Info("Server starting") // Global logger ll.Fields("port", 8080).Info("Listening") // Conditional logging at package level ll.If(simulation).Debug("Test mode") ll.IfErr(err).Error("Startup failed") // Debug utilities ll.Dbg(config) ll.Dump(requestBody) ll.Inspect(complexStruct) } ``` ## Real-World Examples ### Web Server with Structured Logging ```go package main import ( "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "net/http" "time" ) func main() { // Root logger - enabled by default log := ll.New("server") // JSON output for production log.Handler(lh.NewJSONHandler(os.Stdout)) // Request logger with context http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { reqLog := log.Namespace("http").Fields( "method", r.Method, "path", r.URL.Path, "request_id", r.Header.Get("X-Request-ID"), ) start := time.Now() reqLog.Info("request started") // ... handle request ... reqLog.Fields( "status", 200, "duration_ms", time.Since(start).Milliseconds(), ).Info("request completed") }) log.Info("Server listening on :8080") http.ListenAndServe(":8080", nil) } ``` ### Microservice with VictoriaLogs ```go package main import ( "github.com/olekukonko/ll" "github.com/olekukonko/ll/l3rd/victoria" ) func main() { // Production setup vlHandler, _ := victoria.New( victoria.WithURL("http://logs.internal:9428"), victoria.WithAppName("payment-api"), victoria.WithEnvironment("production"), victoria.WithVersion("1.2.3"), victoria.WithBatching(500, 2*time.Second), victoria.WithRetry(3), ) defer vlHandler.Close() logger := ll.New("payment"). Handler(vlHandler). AddContext("region", "us-east-1") logger.Info("Payment service initialized") // Conditional error handling if err := processPayment(); err != nil { logger.IfErr(err). Fields("payment_id", paymentID). Error("Payment processing failed") } } ``` ## Performance `ll` is engineered for high-performance environments: | Operation | Time/op | Allocations | |-----------|---------|-------------| | **Disabled log** | **15.9 ns** | **0 allocs** | | Simple text log | 176 ns | 2 allocs | | With 2 fields | 383 ns | 4 allocs | | JSON output | 1006 ns | 13 allocs | | Namespace lookup (cached) | 550 ns | 6 allocs | | Deduplication | 214 ns | 2 allocs | **Key optimizations**: - Zero allocations when logs are skipped (conditional, disabled) - Atomic operations for hot paths - Sync.Pool for buffer reuse - LRU cache for source file lines (Dbg) - Sharded mutexes for deduplication ## Why Choose `ll`? | Feature | `ll` | `slog` | `zap` | `logrus` | |---------|------|--------|-------|----------| | **Enabled by default** | ✅ | ❌ | ❌ | ❌ | | Hierarchical namespaces | ✅ | ❌ | ❌ | ❌ | | Conditional logging | ✅ | ❌ | ❌ | ❌ | | Error-based conditions | ✅ | ❌ | ❌ | ❌ | | Source-aware Dbg() | ✅ | ❌ | ❌ | ❌ | | Private field inspection | ✅ | ❌ | ❌ | ❌ | | Hex/ASCII Dump() | ✅ | ❌ | ❌ | ❌ | | Middleware pipeline | ✅ | ❌ | ✅ (limited) | ❌ | | Deduplication | ✅ | ❌ | ❌ | ❌ | | Rate limiting | ✅ | ❌ | ❌ | ❌ | | VictoriaLogs support | ✅ | ❌ | ❌ | ❌ | | Syslog support | ✅ | ❌ | ❌ | ✅ | | Zero-allocs disabled logs | ✅ | ❌ | ❌ | ❌ | | Thread-safe | ✅ | ✅ | ✅ | ✅ | ## Documentation - [GoDoc](https://pkg.go.dev/github.com/olekukonko/ll) - Full API documentation - [Examples](_example/) - Runable example code - [Benchmarks](tests/ll_bench_test.go) - Performance benchmarks ## Contributing Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. ## License MIT License - see [LICENSE](LICENSE) for details.golang-github-olekukonko-ll-0.1.8/_example/000077500000000000000000000000001516152337300206335ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/_example/dump.png000066400000000000000000003444361516152337300223240ustar00rootroot00000000000000PNG  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.1.8/_example/hello.txt000066400000000000000000000000131516152337300224710ustar00rootroot00000000000000hello worldgolang-github-olekukonko-ll-0.1.8/_example/stack.png000066400000000000000000003720661516152337300224640ustar00rootroot00000000000000PNG  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.1.8/comb.hcl000066400000000000000000000003651516152337300204550ustar00rootroot00000000000000recursive = true output_file = "all.txt" extensions = [".go"] exclude_dirs { items = ["_examples", "_lab", "_tmp", "pkg", "lab","bin"] } exclude_files { items = ["before.txt","after.txt"] } use_gitignore = true detailed = true go_mode = "all"golang-github-olekukonko-ll-0.1.8/conditional.go000066400000000000000000000431211516152337300216740ustar00rootroot00000000000000package ll import ( "sync" ) // conditionalPool pools Conditional instances to reduce allocations. var conditionalPool = sync.Pool{ New: func() any { return &Conditional{} }, } // 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) } // getConditional retrieves a Conditional from the pool or creates a new one. func getConditional(logger *Logger, condition bool) *Conditional { c := conditionalPool.Get().(*Conditional) c.logger = logger c.condition = condition return c } // putConditional returns a Conditional to the pool for reuse. func putConditional(c *Conditional) { c.logger = nil c.condition = false conditionalPool.Put(c) } // 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 getConditional(l, condition) } // 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 } } // Reuse current instance if condition unchanged if cl.condition == result { return cl } cl.condition = result return cl } // IfErr creates a conditional logger that logs only if the error is non-nil. // It's designed for the common pattern of checking errors before logging. // Example: // // err := doSomething() // logger.IfErr(err).Error("Operation failed") // Only logs if err != nil func (l *Logger) IfErr(err error) *Conditional { return l.If(err != nil) } // IfErrAny creates a conditional logger that logs only if AT LEAST ONE error is non-nil. // It evaluates a variadic list of errors, setting the condition to true if any // is non-nil (logical OR). Useful when any error should trigger logging. // Example: // // err1 := validate(input) // err2 := authorize(user) // logger.IfErrAny(err1, err2).Error("Either check failed") // Logs if EITHER error exists func (l *Logger) IfErrAny(errs ...error) *Conditional { for _, err := range errs { if err != nil { return l.If(true) // Any non-nil error makes it true } } return l.If(false) // False only if all errors are nil } // IfErrOne creates a conditional logger that logs only if ALL errors are non-nil. // It evaluates a variadic list of errors, setting the condition to true only if // all are non-nil (logical AND). Useful when you need all errors to be present. // Example: // // err1 := validate(input) // err2 := authorize(user) // logger.IfErrOne(err1, err2).Error("Both checks failed") // Logs only if BOTH errors exist func (l *Logger) IfErrOne(errs ...error) *Conditional { for _, err := range errs { if err == nil { return l.If(false) // Any nil error makes it false } } return l.If(len(errs) > 0) // True only if we have at least one error and all are non-nil } // IfErr creates a conditional logger that logs only if the error is non-nil. // Returns a new Conditional with the error check result. // Example: // // err := doSomething() // logger.If(true).IfErr(err).Error("Failed") // Only logs if condition true AND err != nil func (cl *Conditional) IfErr(err error) *Conditional { return cl.IfOne(err != nil) } // IfErrAny creates a conditional logger that logs only if AT LEAST ONE error is non-nil. // Returns a new Conditional with the logical OR result of error checks. // Example: // // err1 := validate(input) // err2 := authorize(user) // logger.If(true).IfErrAny(err1, err2).Error("Either failed") // Logs if condition true AND either error exists func (cl *Conditional) IfErrAny(errs ...error) *Conditional { for _, err := range errs { if err != nil { // Reuse if condition already true if cl.condition { return cl } cl.condition = true return cl } } cl.condition = false return cl } // IfErrOne creates a conditional logger that logs only if ALL errors are non-nil. // Returns a new Conditional with the logical AND result of error checks. // Example: // // err1 := validate(input) // err2 := authorize(user) // logger.If(true).IfErrOne(err1, err2).Error("Both failed") // Logs if condition true AND both errors exist func (cl *Conditional) IfErrOne(errs ...error) *Conditional { for _, err := range errs { if err == nil { cl.condition = false return cl } } if len(errs) > 0 { cl.condition = cl.condition && true } return cl } // 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 } } // Reuse current instance if condition unchanged if cl.condition == result { return cl } cl.condition = result return cl } // 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, optimizing performance. 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...) } // 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...) } // 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...) } // 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 - return FieldBuilder with nil logger // so that all subsequent logging methods become no-ops if !cl.condition { return &FieldBuilder{logger: nil, fields: nil} } // Delegate to logger's Field method return cl.logger.Field(fields) } // 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 - return FieldBuilder with nil logger // so that all subsequent logging methods become no-ops if !cl.condition { return &FieldBuilder{logger: nil, fields: nil} } // Delegate to logger's Fields method return cl.logger.Fields(pairs...) } // 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...) } // 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...) } // 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...) } // 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...) } golang-github-olekukonko-ll-0.1.8/dbg.go000066400000000000000000000157161516152337300201360ustar00rootroot00000000000000package ll import ( "container/list" "fmt" "os" "runtime" "strings" "sync" "github.com/olekukonko/ll/lx" ) // ----------------------------------------------------------------------------- // Global Cache Instance // ----------------------------------------------------------------------------- // sourceCache caches up to 128 source files using LRU eviction. var sourceCache = newFileLRU(128) // ----------------------------------------------------------------------------- // File-Level LRU Cache // ----------------------------------------------------------------------------- type fileLRU struct { capacity int mu sync.Mutex list *list.List items map[string]*list.Element } type fileItem struct { key string lines []string } func newFileLRU(capacity int) *fileLRU { if capacity <= 0 { capacity = 1 } return &fileLRU{ capacity: capacity, list: list.New(), items: make(map[string]*list.Element, capacity), } } // getLine retrieves a specific 1-indexed line from a file. func (c *fileLRU) getLine(file string, line int) (string, bool) { c.mu.Lock() defer c.mu.Unlock() // 1. Cache Hit if elem, ok := c.items[file]; ok { c.list.MoveToFront(elem) item := elem.Value.(*fileItem) if item.lines == nil { return "", false } return nthLine(item.lines, line) } // 2. Cache Miss - Read File // Release lock during I/O to avoid blocking other loggers c.mu.Unlock() data, err := os.ReadFile(file) c.mu.Lock() // 3. Double-check (another goroutine might have loaded it while unlocked) if elem, ok := c.items[file]; ok { c.list.MoveToFront(elem) item := elem.Value.(*fileItem) if item.lines == nil { return "", false } return nthLine(item.lines, line) } var lines []string if err == nil { lines = strings.Split(string(data), "\n") } // 4. Store (Positive or Negative Cache) item := &fileItem{ key: file, lines: lines, } elem := c.list.PushFront(item) c.items[file] = elem // 5. Evict if needed if c.list.Len() > c.capacity { old := c.list.Back() if old != nil { c.list.Remove(old) delete(c.items, old.Value.(*fileItem).key) } } if lines == nil { return "", false } return nthLine(lines, line) } // nthLine returns the 1-indexed line from slice. func nthLine(lines []string, n int) (string, bool) { if n <= 0 || n > len(lines) { return "", false } return strings.TrimSuffix(lines[n-1], "\r"), true } // ----------------------------------------------------------------------------- // Logger Debug Implementation // ----------------------------------------------------------------------------- // Dbg logs debug information including source file, line number, // and the best-effort extracted expression. // // Example: // // x := 42 // logger.Dbg("val", x) // Output: [file.go:123] "val" = "val", x = 42 func (l *Logger) Dbg(values ...interface{}) { if !l.shouldLog(lx.LevelInfo) { return } l.dbg(2, values...) } func (l *Logger) dbg(skip int, values ...interface{}) { file, line, ok := callerFrame(skip) if !ok { // Fallback if we can't get frame var sb strings.Builder sb.WriteString("[?:?] ") for i, v := range values { if i > 0 { sb.WriteString(", ") } sb.WriteString(fmt.Sprintf("%+v", v)) } l.log(lx.LevelInfo, lx.ClassText, sb.String(), nil, false) return } shortFile := file if idx := strings.LastIndex(file, "/"); idx >= 0 { shortFile = file[idx+1:] } srcLine, hit := sourceCache.getLine(file, line) var expr string if hit && srcLine != "" { // Attempt to extract the text inside Dbg(...) if a := strings.Index(srcLine, "Dbg("); a >= 0 { rest := srcLine[a+len("Dbg("):] if b := strings.LastIndex(rest, ")"); b >= 0 { expr = strings.TrimSpace(rest[:b]) } } else { // Fallback: extract first (...) group if Dbg isn't explicit prefix a := strings.Index(srcLine, "(") b := strings.LastIndex(srcLine, ")") if a >= 0 && b > a { expr = strings.TrimSpace(srcLine[a+1 : b]) } } } // Format output var outBuilder strings.Builder outBuilder.WriteString(fmt.Sprintf("[%s:%d] ", shortFile, line)) // Attempt to split expressions to map 1:1 with values var parts []string if expr != "" { parts = splitExpressions(expr) } // If the number of extracted expressions matches the number of values, // print them as "expr = value". Otherwise, fall back to "expr = val1, val2". if len(parts) == len(values) { for i, v := range values { if i > 0 { outBuilder.WriteString(", ") } outBuilder.WriteString(fmt.Sprintf("%s = %+v", parts[i], v)) } } else { if expr != "" { outBuilder.WriteString(expr) outBuilder.WriteString(" = ") } for i, v := range values { if i > 0 { outBuilder.WriteString(", ") } outBuilder.WriteString(fmt.Sprintf("%+v", v)) } } l.log(lx.LevelInfo, lx.ClassDbg, outBuilder.String(), nil, false) } // splitExpressions splits a comma-separated string of expressions, // respecting nested parentheses, brackets, braces, and quotes. // Example: "a, fn(b, c), d" -> ["a", "fn(b, c)", "d"] func splitExpressions(s string) []string { var parts []string var current strings.Builder depth := 0 // Tracks nested (), [], {} inQuote := false // Tracks string literals var quoteChar rune for _, r := range s { switch { case inQuote: current.WriteRune(r) if r == quoteChar { // We rely on the fact that valid Go source won't have unescaped quotes easily // accessible here without complex parsing, but for simple Dbg calls this suffices. // A robust parser handles `\"`, but simple state toggling covers 99% of debug cases. inQuote = false } case r == '"' || r == '\'': inQuote = true quoteChar = r current.WriteRune(r) case r == '(' || r == '{' || r == '[': depth++ current.WriteRune(r) case r == ')' || r == '}' || r == ']': depth-- current.WriteRune(r) case r == ',' && depth == 0: // Split point parts = append(parts, strings.TrimSpace(current.String())) current.Reset() default: current.WriteRune(r) } } if current.Len() > 0 { parts = append(parts, strings.TrimSpace(current.String())) } return parts } // ----------------------------------------------------------------------------- // Caller Resolution // ----------------------------------------------------------------------------- // callerFrame walks stack frames until it finds the first frame // outside the ll package. func callerFrame(skip int) (file string, line int, ok bool) { // +2 to skip callerFrame + dbg itself. pcs := make([]uintptr, 32) n := runtime.Callers(skip+2, pcs) if n == 0 { return "", 0, false } frames := runtime.CallersFrames(pcs[:n]) for { fr, more := frames.Next() // fr.Function looks like: "github.com/you/mod/ll.(*Logger).Dbg" // We want the first frame that is NOT inside package ll. if fr.Function == "" || !strings.Contains(fr.Function, "/ll.") && !strings.Contains(fr.Function, ".ll.") { return fr.File, fr.Line, true } if !more { // Fallback: return the last frame we saw return fr.File, fr.Line, fr.File != "" } } } golang-github-olekukonko-ll-0.1.8/field.go000066400000000000000000000311521516152337300204550ustar00rootroot00000000000000package ll import ( "fmt" "os" "strings" "sync" "github.com/olekukonko/cat" "github.com/olekukonko/ll/lx" ) // fieldBuilderPool pools FieldBuilder instances to reduce allocations. var fieldBuilderPool = sync.Pool{ New: func() any { return &FieldBuilder{ fields: make(lx.Fields, 0, 8), // Pre-allocate common size } }, } // 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 lx.Fields // Fields to include in the log entry as ordered key-value pairs } // getFieldBuilder retrieves a FieldBuilder from the pool or creates a new one. func getFieldBuilder(logger *Logger, capacity int) *FieldBuilder { fb := fieldBuilderPool.Get().(*FieldBuilder) fb.logger = logger // Ensure minimum capacity to reduce small allocations const minFieldCapacity = 4 if capacity < minFieldCapacity { capacity = minFieldCapacity } if cap(fb.fields) < capacity { fb.fields = make(lx.Fields, 0, capacity) } else { fb.fields = fb.fields[:0] // Reset but keep capacity } return fb } // putFieldBuilder returns a FieldBuilder to the pool for reuse. func putFieldBuilder(fb *FieldBuilder) { fb.logger = nil fb.fields = fb.fields[:0] fieldBuilderPool.Put(fb) } // 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 { // If logger is nil (e.g., from a false Conditional), return nil if fb.logger == nil { return nil } // Clone the parent logger to preserve its configuration newLogger := fb.logger.Clone() // Copy builder's fields into the new logger's context (optimized) if len(fb.fields) > 0 { newLogger.context = append(lx.Fields(nil), fb.fields...) } 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. // 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) { if fb.logger == nil { return } fb.logger.log(lx.LevelInfo, lx.ClassText, cat.Space(args...), fb.fields, false) putFieldBuilder(fb) } // 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. This method is part of the fluent API. // 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) { if fb.logger == nil { return } msg := fmt.Sprintf(format, args...) fb.logger.log(lx.LevelInfo, lx.ClassText, msg, fb.fields, false) putFieldBuilder(fb) } // 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. // 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) { if fb.logger == nil { return } fb.logger.log(lx.LevelDebug, lx.ClassText, cat.Space(args...), fb.fields, false) putFieldBuilder(fb) } // Debugf logs a message at Debug level with the builder's fields. // It formats the message and delegates to the logger's log method. // This method is used for debugging information that may be disabled in production. // 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) { if fb.logger == nil { return } msg := fmt.Sprintf(format, args...) fb.logger.log(lx.LevelDebug, lx.ClassText, msg, fb.fields, false) putFieldBuilder(fb) } // 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. // 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) { if fb.logger == nil { return } fb.logger.log(lx.LevelWarn, lx.ClassText, cat.Space(args...), fb.fields, false) putFieldBuilder(fb) } // Warnf logs a message at Warn level with the builder's fields. // It formats the message and delegates to the logger's log method. // 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) { if fb.logger == nil { return } msg := fmt.Sprintf(format, args...) fb.logger.log(lx.LevelWarn, lx.ClassText, msg, fb.fields, false) putFieldBuilder(fb) } // 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. // 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) { if fb.logger == nil { return } fb.logger.log(lx.LevelError, lx.ClassText, cat.Space(args...), fb.fields, false) putFieldBuilder(fb) } // Errorf logs a message at Error level with the builder's fields. // It formats the message and delegates to the logger's log method. // 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) { if fb.logger == nil { return } msg := fmt.Sprintf(format, args...) fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, false) putFieldBuilder(fb) } // 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. // 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) { if fb.logger == nil { return } fb.logger.log(lx.LevelError, lx.ClassText, cat.Space(args...), fb.fields, true) putFieldBuilder(fb) } // 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. // 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) { if fb.logger == nil { return } msg := fmt.Sprintf(format, args...) fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, true) putFieldBuilder(fb) } // 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. 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) { if fb.logger == nil { return } var builder strings.Builder for i, arg := range args { if i > 0 { builder.WriteString(lx.Space) } builder.WriteString(fmt.Sprint(arg)) } fb.logger.log(lx.LevelFatal, lx.ClassText, builder.String(), fb.fields, fb.logger.fatalStack) if fb.logger.fatalExits { os.Exit(1) } putFieldBuilder(fb) } // Fatalf logs a formatted message at Error level with a stack trace and the builder's fields, // then exits. It delegates to Fatal. 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) { if fb.logger == nil { return } 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. This method is used for critical errors. // 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) { if fb.logger == nil { return } var builder strings.Builder for i, arg := range args { if i > 0 { builder.WriteString(lx.Space) } builder.WriteString(fmt.Sprint(arg)) } msg := builder.String() fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, true) 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. This method is used for critical errors. // 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) { if fb.logger == nil { return } 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. Returns the FieldBuilder for chaining. // 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") func (fb *FieldBuilder) Err(errs ...error) *FieldBuilder { if fb.logger == nil { return fb } 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 { fb.fields = append(fb.fields, lx.Field{Key: "error", Value: nonNilErrors[0]}) } else { fb.fields = append(fb.fields, lx.Field{Key: "error", Value: nonNilErrors}) } fb.logger.log(lx.LevelError, lx.ClassText, builder.String(), nil, false) } return fb } // Merge adds additional key-value pairs to the FieldBuilder. // It processes variadic arguments as key-value pairs, expecting string keys. // Returns the FieldBuilder for chaining. // 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 { // Merge can work even with nil logger since it just manipulates fields for i := 0; i < len(pairs)-1; i += 2 { if key, ok := pairs[i].(string); ok { fb.fields = append(fb.fields, lx.Field{Key: key, Value: pairs[i+1]}) } else { fb.fields = append(fb.fields, lx.Field{ Key: "error", Value: fmt.Errorf("non-string key in Merge: %v", pairs[i]), }) } } if len(pairs)%2 != 0 { fb.fields = append(fb.fields, lx.Field{ Key: "error", Value: fmt.Errorf("uneven key-value pairs in Merge: [%v]", pairs[len(pairs)-1]), }) } return fb } golang-github-olekukonko-ll-0.1.8/global.go000066400000000000000000000631241516152337300206360ustar00rootroot00000000000000package ll import ( "sync/atomic" "time" "github.com/olekukonko/ll/lx" ) // 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 = New("") // 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.LevelInfo) // ll.Warn("Ignored") // Inactive output // ll.Info("Logged") // Output: [] INFO: 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") // Inactive 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. func If(condition bool) *Conditional { return defaultLogger.If(condition) } // IfErr creates a conditional logger that logs only if the error is non-nil using the default logger. func IfErr(err error) *Conditional { return defaultLogger.IfErr(err) } // IfErrAny creates a conditional logger that logs only if AT LEAST ONE error is non-nil using the default logger. func IfErrAny(errs ...error) *Conditional { return defaultLogger.IfErrAny(errs...) } // IfErrOne creates a conditional logger that logs only if ALL errors are non-nil using the default logger. func IfErrOne(errs ...error) *Conditional { return defaultLogger.IfErrOne(errs...) } // 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(pairs ...any) *Logger { return defaultLogger.AddContext(pairs...) } // 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"}k 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") // Inactive 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("Inactive middleware") // Output: [] INFO: Inactive 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 { return defaultLogger.Measure(fns...) } // Labels temporarily attaches one or more label names to the logger for the next log entry. // Labels are typically used for metrics, benchmarking, tracing, or categorizing logs in a structured way. // // The labels are stored atomically and intended to be short-lived, applying only to the next // log operation (or until overwritten by a subsequent call to Labels). Multiple labels can // be provided as separate string arguments. // // Example usage: // // logger := New("app").Enable() // // // Add labels for a specific operation // logger.Labels("load_users", "process_orders").Measure(func() { // // ... perform work ... // }, func() { // // ... optional callback ... // }) func Labels(names ...string) *Logger { return defaultLogger.Labels(names...) } // Since creates a timer that will log the duration when completed // If startTime is provided, uses that as the start time; otherwise uses time.Now() // // defer logger.Since().Info("request") // Auto-start // logger.Since(start).Info("request") // Manual timing // logger.Since().If(debug).Debug("timing") // Conditional func Since(start ...time.Time) *SinceBuilder { return defaultLogger.Since(start...) } // 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.Benchmark(start) } // 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") // Inactive 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") // Inactive 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") // Inactive 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") // Inactive 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(values ...interface{}) { defaultLogger.Dump(values...) } // 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...) } // Output logs data in a human-readable JSON format at Info level, including caller file and line information. // It is similar to Dbg but formats the output as JSON for better readability. It is thread-safe and respects // the logger’s configuration (e.g., enabled, level, suspend, handler, middleware). func Output(values ...interface{}) { defaultLogger.output(2, values...) } // Inspect logs one or more values in a **developer-friendly, deeply introspective format** at Info level. // It includes the caller file and line number, and reveals **all fields** — including: func Inspect(values ...interface{}) { o := NewInspector(defaultLogger) o.Log(2, values...) } func Apply(opts ...Option) *Logger { return defaultLogger.Apply(opts...) } func Toggle(v bool) *Logger { return defaultLogger.Toggle(v) } golang-github-olekukonko-ll-0.1.8/go.mod000066400000000000000000000003371516152337300201520ustar00rootroot00000000000000module github.com/olekukonko/ll go 1.21 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect ) golang-github-olekukonko-ll-0.1.8/go.sum000066400000000000000000000023341516152337300201760ustar00rootroot00000000000000github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/olekukonko/cat v0.0.0-20250808185521-0d9f5959211f h1:ttD8kf/e7ZWCoQohfY/5qhUuoUfjmCgaGL/5zRNwM4c= github.com/olekukonko/cat v0.0.0-20250808185521-0d9f5959211f/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/cat v0.0.0-20250808191157-46fba99501f3 h1:3o1yj5hu9LOTwjey1RE0IBaB4u1HCLbD+lDPhJrl8Y4= github.com/olekukonko/cat v0.0.0-20250808191157-46fba99501f3/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/cat v0.0.0-20250908003013-b0de306c343b h1:ETHdAZIK6j939sa2x8/NlbU8OKn5Cotyjxn1VRypLhw= github.com/olekukonko/cat v0.0.0-20250908003013-b0de306c343b/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= golang-github-olekukonko-ll-0.1.8/inspector.go000066400000000000000000000167601516152337300214100ustar00rootroot00000000000000package ll import ( "encoding/json" "fmt" "reflect" "runtime" "strings" "unsafe" "github.com/olekukonko/ll/lx" ) // Inspector is a utility for Logger that provides advanced inspection and logging of data // in human-readable JSON format. It uses reflection to access and represent unexported fields, // nested structs, embedded structs, and pointers, making it useful for debugging complex data structures. type Inspector struct { logger *Logger } // NewInspector returns a new Inspector instance associated with the provided logger. func NewInspector(logger *Logger) *Inspector { return &Inspector{logger: logger} } // Log outputs the given values as indented JSON at the Info level, prefixed with the caller's // file name and line number. It handles structs (including unexported fields, nested, and embedded), // pointers, errors, and other types. The skip parameter determines how many stack frames to skip // when identifying the caller; typically set to 2 to account for the call to Log and its wrapper. // // Example usage within a Logger method: // // o := NewInspector(l) // o.Log(2, someStruct) func (o *Inspector) Log(skip int, values ...interface{}) { // Skip if logger is suspended or Info level is disabled if o.logger.suspend.Load() || !o.logger.shouldLog(lx.LevelInfo) { return } // Retrieve caller information for logging context _, file, line, ok := runtime.Caller(skip) if !ok { o.logger.log(lx.LevelError, lx.ClassText, "Inspector: Unable to parse runtime caller", nil, false) return } // Extract short filename for concise output shortFile := file if idx := strings.LastIndex(file, "/"); idx >= 0 { shortFile = file[idx+1:] } // Process each value individually for _, value := range values { var jsonData []byte var err error // Use reflection for struct types to handle unexported and nested fields val := reflect.ValueOf(value) if val.Kind() == reflect.Ptr { val = val.Elem() } if val.Kind() == reflect.Struct { valueMap := o.structToMap(val) jsonData, err = json.MarshalIndent(valueMap, "", " ") } else if errVal, ok := value.(error); ok { // Special handling for errors to represent them as a simple map value = map[string]string{"error": errVal.Error()} jsonData, err = json.MarshalIndent(value, "", " ") } else { // Fall back to standard JSON marshaling for non-struct types jsonData, err = json.MarshalIndent(value, "", " ") } if err != nil { o.logger.log(lx.LevelError, lx.ClassInspect, fmt.Sprintf("Inspector: JSON encoding error: %v", err), nil, false) continue } // Construct log message with file, line, and JSON data msg := fmt.Sprintf("[%s:%d] %s", shortFile, line, string(jsonData)) o.logger.log(lx.LevelInfo, lx.ClassInspect, msg, nil, false) } } // structToMap recursively converts a struct's reflect.Value to a map[string]interface{}. // It includes unexported fields (named with parentheses), prefixes pointers with '*', // flattens anonymous embedded structs without json tags, and uses unsafe pointers to access // unexported primitive fields when reflect.CanInterface() returns false. func (o *Inspector) structToMap(val reflect.Value) map[string]interface{} { result := make(map[string]interface{}) if !val.IsValid() { return result } typ := val.Type() for i := 0; i < val.NumField(); i++ { field := val.Field(i) fieldType := typ.Field(i) // Determine field name: prefer json tag if present and not "-", else use struct field name baseName := fieldType.Name jsonTag := fieldType.Tag.Get("json") hasJsonTag := false if jsonTag != "" { if idx := strings.Index(jsonTag, ","); idx != -1 { jsonTag = jsonTag[:idx] } if jsonTag != "-" { baseName = jsonTag hasJsonTag = true } } // Enclose unexported field names in parentheses fieldName := baseName if !fieldType.IsExported() { fieldName = "(" + baseName + ")" } // Handle pointer fields isPtr := fieldType.Type.Kind() == reflect.Ptr if isPtr { fieldName = "*" + fieldName if field.IsNil() { result[fieldName] = nil continue } field = field.Elem() } // Recurse for struct fields if field.Kind() == reflect.Struct { subMap := o.structToMap(field) isNested := !fieldType.Anonymous || hasJsonTag if isNested { result[fieldName] = subMap } else { // Flatten embedded struct fields into the parent map, avoiding overwrites for k, v := range subMap { if _, exists := result[k]; !exists { result[k] = v } } } } else { // Handle primitive fields if field.CanInterface() { result[fieldName] = field.Interface() } else { // Use unsafe access for unexported primitives ptr := getDataPtr(field) switch field.Kind() { case reflect.String: result[fieldName] = *(*string)(ptr) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: result[fieldName] = o.getIntFromUnexportedField(field) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: result[fieldName] = o.getUintFromUnexportedField(field) case reflect.Float32, reflect.Float64: result[fieldName] = o.getFloatFromUnexportedField(field) case reflect.Bool: result[fieldName] = *(*bool)(ptr) default: result[fieldName] = fmt.Sprintf("*unexported %s*", field.Type().String()) } } } } return result } // emptyInterface represents the internal structure of an empty interface{}. // This is used for unsafe pointer manipulation to access unexported field data. type emptyInterface struct { typ unsafe.Pointer word unsafe.Pointer } // getDataPtr returns an unsafe.Pointer to the underlying data of a reflect.Value. // This enables direct access to unexported fields via unsafe operations. func getDataPtr(v reflect.Value) unsafe.Pointer { return (*emptyInterface)(unsafe.Pointer(&v)).word } // getIntFromUnexportedField extracts a signed integer value from an unexported field // using unsafe pointer access. It supports int, int8, int16, int32, and int64 kinds, // returning the value as int64. Returns 0 for unsupported kinds. func (o *Inspector) getIntFromUnexportedField(field reflect.Value) int64 { ptr := getDataPtr(field) switch field.Kind() { case reflect.Int: return int64(*(*int)(ptr)) case reflect.Int8: return int64(*(*int8)(ptr)) case reflect.Int16: return int64(*(*int16)(ptr)) case reflect.Int32: return int64(*(*int32)(ptr)) case reflect.Int64: return *(*int64)(ptr) } return 0 } // getUintFromUnexportedField extracts an unsigned integer value from an unexported field // using unsafe pointer access. It supports uint, uint8, uint16, uint32, and uint64 kinds, // returning the value as uint64. Returns 0 for unsupported kinds. func (o *Inspector) getUintFromUnexportedField(field reflect.Value) uint64 { ptr := getDataPtr(field) switch field.Kind() { case reflect.Uint: return uint64(*(*uint)(ptr)) case reflect.Uint8: return uint64(*(*uint8)(ptr)) case reflect.Uint16: return uint64(*(*uint16)(ptr)) case reflect.Uint32: return uint64(*(*uint32)(ptr)) case reflect.Uint64: return *(*uint64)(ptr) } return 0 } // getFloatFromUnexportedField extracts a floating-point value from an unexported field // using unsafe pointer access. It supports float32 and float64 kinds, returning the value // as float64. Returns 0 for unsupported kinds. func (o *Inspector) getFloatFromUnexportedField(field reflect.Value) float64 { ptr := getDataPtr(field) switch field.Kind() { case reflect.Float32: return float64(*(*float32)(ptr)) case reflect.Float64: return *(*float64)(ptr) } return 0 } golang-github-olekukonko-ll-0.1.8/l3rd/000077500000000000000000000000001516152337300177055ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/l3rd/syslog/000077500000000000000000000000001516152337300212255ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/l3rd/syslog/syslogger.go000066400000000000000000000164711516152337300236030ustar00rootroot00000000000000//go:build !windows package syslog import ( "fmt" "log/syslog" "strings" "github.com/olekukonko/ll/lx" ) // Config holds configuration for Syslog handler. type Config struct { // Tag is the application identifier (max 32 chars, will be truncated if longer). // Default: "golang-app" Tag string // Facility is the syslog facility (e.g., syslog.LOG_USER, syslog.LOG_LOCAL0). // Default: syslog.LOG_USER Facility syslog.Priority // Priority is the optional initial priority (defaults to syslog.LOG_INFO). // Default: syslog.LOG_INFO Priority syslog.Priority // Network is the network protocol for remote syslog ("tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"). // If set, connects to remote syslog; otherwise, uses local. // Default: "" (local) Network string // Addr is the remote address for syslog (e.g., "logs.example.com:514"). // Required if Network is set. // Default: "" Addr string } // Option is a function that modifies Config. type Option func(*Config) // WithTag sets the application tag/ident. func WithTag(tag string) Option { return func(c *Config) { c.Tag = tag } } // WithFacility sets the syslog facility. func WithFacility(facility syslog.Priority) Option { return func(c *Config) { c.Facility = facility } } // WithPriority sets the initial priority. func WithPriority(priority syslog.Priority) Option { return func(c *Config) { c.Priority = priority } } // WithRemote sets the network and address for remote syslog. func WithRemote(network, addr string) Option { return func(c *Config) { c.Network = network c.Addr = addr } } // Syslog is an lx.Handler that sends log entries to the system syslog daemon. // It integrates with the local syslog service (via Unix sockets or network) and maps // log levels to appropriate syslog priority levels. This handler is useful for // applications that need to integrate with system monitoring tools or centralized // log management solutions. // // The handler supports: // - Automatic level mapping (lx.LevelType to syslog.Priority) // - Configurable tag/ident for log identification // - Both local and remote syslog destinations // - Structured fields as part of log message // // Example: // // handler, err := syslog.New( // syslog.WithTag("myapp"), // syslog.WithFacility(syslog.LOG_LOCAL0), // ) // if err != nil { // log.Fatal(err) // } // logger := ll.New("app").Enable().Handler(handler) // logger.Info("Application started") // Sent to syslog type Syslog struct { writer *syslog.Writer // Underlying syslog writer tag string // Application tag/ident for syslog entries } // New creates a new Syslog handler based on the provided options. // It connects to either local or remote syslog depending on configuration. // Returns a configured Syslog or an error if connection fails. // // Example: // // handler, err := syslog.New( // syslog.WithTag("my-service"), // syslog.WithFacility(syslog.LOG_LOCAL0), // syslog.WithRemote("tcp", "logs.company.com:6514"), // ) // if err != nil { // return err // } func New(opts ...Option) (*Syslog, error) { // Initialize default configuration config := &Config{ Tag: "golang-app", Facility: syslog.LOG_USER, Priority: syslog.LOG_INFO, Network: "", Addr: "", } // Apply provided options for _, opt := range opts { opt(config) } // Truncate tag to syslog limits (typically 32 chars) if len(config.Tag) > 32 { config.Tag = config.Tag[:32] } var writer *syslog.Writer var err error if config.Network != "" && config.Addr != "" { // Connect to remote syslog writer, err = syslog.Dial(config.Network, config.Addr, config.Facility|config.Priority, config.Tag) if err != nil { return nil, fmt.Errorf("failed to connect to remote syslog at %s://%s: %w", config.Network, config.Addr, err) } } else { // Connect to local syslog writer, err = syslog.New(config.Facility|config.Priority, config.Tag) if err != nil { return nil, fmt.Errorf("failed to connect to local syslog: %w", err) } } return &Syslog{ writer: writer, tag: config.Tag, }, nil } // Handle implements the lx.Handler interface for Syslog. // It receives log entries, maps lx log levels to syslog priorities, formats the // message with namespace and fields, and sends it to syslog. Thread-safe via the // underlying syslog.Writer implementation. // // Returns nil on successful delivery, or an error if syslog write fails. // // Example (internal usage): // // handler.Handle(&lx.Entry{Message: "error occurred", Level: lx.LevelError}) func (h *Syslog) Handle(e *lx.Entry) error { // Map lx level to syslog priority priority := h.mapLevelToPriority(e.Level) // Build formatted message message := h.formatMessage(e) // Write to syslog based on priority var err error switch priority { case syslog.LOG_EMERG: err = h.writer.Emerg(message) case syslog.LOG_ALERT: err = h.writer.Alert(message) case syslog.LOG_CRIT: err = h.writer.Crit(message) case syslog.LOG_ERR: err = h.writer.Err(message) case syslog.LOG_WARNING: err = h.writer.Warning(message) case syslog.LOG_NOTICE: err = h.writer.Notice(message) case syslog.LOG_INFO: err = h.writer.Info(message) case syslog.LOG_DEBUG: err = h.writer.Debug(message) default: err = h.writer.Info(message) } return err } // Close closes the connection to the syslog daemon. // It should be called when the handler is no longer needed to release system resources. // Returns nil if successful, or an error if the close operation fails. func (h *Syslog) Close() error { return h.writer.Close() } // Timestamped implements the lx.Timestamper interface. // This is a no-op for Syslog handler since timestamps are handled by syslog. // The method exists for interface compatibility. // // Parameters: // - enable: Ignored // - format: Ignored func (h *Syslog) Timestamped(enable bool, format ...string) { // Syslog handles timestamps internally } // mapLevelToPriority maps lx.LevelType to syslog.Priority. // This mapping determines how log levels are represented in the syslog system, // affecting filtering, routing, and alerting in log management tools. func (h *Syslog) mapLevelToPriority(level lx.LevelType) syslog.Priority { switch level { case lx.LevelDebug: return syslog.LOG_DEBUG case lx.LevelInfo: return syslog.LOG_INFO case lx.LevelWarn: return syslog.LOG_WARNING case lx.LevelError, lx.LevelFatal: return syslog.LOG_ERR default: return syslog.LOG_INFO } } // formatMessage formats an lx.Entry into a string suitable for syslog. // It includes the namespace, message, and structured fields in a readable format. // Fields are appended as key=value pairs for easy parsing by log analysis tools. func (h *Syslog) formatMessage(e *lx.Entry) string { var builder strings.Builder // Add namespace if present if e.Namespace != "" { builder.WriteString("[") builder.WriteString(e.Namespace) builder.WriteString("] ") } // Add main message builder.WriteString(e.Message) // Add fields if present if len(e.Fields) > 0 { builder.WriteString(" [") first := true for _, f := range e.Fields { if !first { builder.WriteString(" ") } builder.WriteString(f.Key) builder.WriteString("=") builder.WriteString(fmt.Sprint(f.Value)) first = false } builder.WriteString("]") } // Add stack trace if present if len(e.Stack) > 0 { builder.WriteString("\nStack trace:\n") builder.Write(e.Stack) } return builder.String() } golang-github-olekukonko-ll-0.1.8/l3rd/syslog/syslogger_windows.go000066400000000000000000000027271516152337300253540ustar00rootroot00000000000000//go:build windows package syslog import ( "fmt" "github.com/olekukonko/ll/lx" ) // Config holds configuration for Syslog handler (Windows stub). type Config struct { Tag string Facility int Priority int Network string Addr string } // Option is a function that modifies Config. type Option func(*Config) // WithTag sets the application tag/ident. func WithTag(tag string) Option { return func(c *Config) { c.Tag = tag } } // WithFacility sets the syslog facility. func WithFacility(facility int) Option { return func(c *Config) { c.Facility = facility } } // WithPriority sets the initial priority. func WithPriority(priority int) Option { return func(c *Config) { c.Priority = priority } } // WithRemote sets the network and address for remote syslog. func WithRemote(network, addr string) Option { return func(c *Config) { c.Network = network c.Addr = addr } } // Syslog is a stub handler for Windows (syslog not supported). type Syslog struct{} // New creates a new Syslog handler - returns error on Windows. func New(opts ...Option) (*Syslog, error) { return nil, fmt.Errorf("syslog is not supported on Windows") } // Handle implements the lx.Handler interface (stub). func (h *Syslog) Handle(e *lx.Entry) error { return nil } // Close closes the connection (stub). func (h *Syslog) Close() error { return nil } // Timestamped implements the lx.Timestamper interface (stub). func (h *Syslog) Timestamped(enable bool, format ...string) {} golang-github-olekukonko-ll-0.1.8/l3rd/victoria/000077500000000000000000000000001516152337300215255ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/l3rd/victoria/options.go000066400000000000000000000115251516152337300235530ustar00rootroot00000000000000package victoria import ( "net/http" "time" ) // Option is a function that modifies Config. // Used with the New() constructor for flexible configuration. // Multiple options can be chained together. // // Example: // // handler, err := victoria.New( // victoria.WithURL("http://logs.example.com"), // victoria.WithAppName("api-server"), // victoria.WithEnvironment("production"), // victoria.WithBatching(100, 2*time.Second), // ) type Option func(*Config) // WithURL sets the VictoriaLogs endpoint URL. // The URL should point to VictoriaLogs' JSON ingestion endpoint. // Default: "http://localhost:9428/insert/jsonline" // // Example: // // victoria.WithURL("http://victoria-logs.prod.svc:9428/insert/jsonline") func WithURL(url string) Option { return func(c *Config) { c.URL = url } } // WithAppName sets the application name for log identification. // This appears in the "app" field of all log entries and is used as // a stream label for efficient querying in VictoriaLogs. // Default: executable name (without extension) // // Example: // // victoria.WithAppName("order-service") func WithAppName(name string) Option { return func(c *Config) { c.AppName = name } } // WithVersion sets the application version. // Useful for tracking deployments and version-specific issues. // Default: "unknown" // // Example: // // victoria.WithVersion("2.1.0") func WithVersion(version string) Option { return func(c *Config) { c.Version = version } } // WithEnvironment sets the deployment environment. // Common values: "production", "staging", "development", "testing" // This field is used as a stream label for environment-based filtering. // Default: "production" // // Example: // // victoria.WithEnvironment("staging") func WithEnvironment(env string) Option { return func(c *Config) { c.Environment = env } } // WithHostname sets the hostname for log entries. // Useful in distributed systems to identify which server or pod generated the log. // Default: os.Hostname() result // // Example: // // victoria.WithHostname("web-server-01") func WithHostname(hostname string) Option { return func(c *Config) { c.Hostname = hostname } } // WithStreamKeys sets the fields to use as stream labels in VictoriaLogs. // Stream labels enable efficient partitioning and querying of logs. // Common stream keys: ["app", "env", "level", "ns", "host"] // Default: ["app", "env", "level", "ns", "host"] // // Example: // // // Use application and environment as primary stream labels // victoria.WithStreamKeys("app", "env", "level") func WithStreamKeys(keys ...string) Option { return func(c *Config) { c.StreamKeys = keys } } // WithFieldMapping adds a custom field name mapping. // Useful for renaming fields to match existing log schemas or conventions. // The mapping is applied to all log entries processed by the handler. // // Example: // // // Rename "user_id" field to "userId" // victoria.WithFieldMapping("user_id", "userId") func WithFieldMapping(from, to string) Option { return func(c *Config) { c.FieldMap[from] = to } } // WithHTTPClient sets a custom HTTP client for VictoriaLogs requests. // Use this to customize timeouts, authentication, TLS configuration, // or to use a proxy. If not set, a default client with reasonable // timeouts is created. // // Example: // // client := &http.Client{ // Timeout: 30 * time.Second, // Transport: &http.Transport{ // TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // }, // } // victoria.WithHTTPClient(client) func WithHTTPClient(client *http.Client) Option { return func(c *Config) { c.HTTPClient = client } } // WithTimeout sets the HTTP request timeout. // This timeout applies to both connection establishment and request completion. // Consider network latency and VictoriaLogs processing time when setting this. // Default: 5 seconds // // Example: // // // Allow more time for cross-region requests // victoria.WithTimeout(10 * time.Second) func WithTimeout(timeout time.Duration) Option { return func(c *Config) { c.Timeout = timeout } } // WithRetry sets the number of retry attempts for failed requests. // Retries use exponential backoff (0ms, 100ms, 400ms, ...). // Note: 4xx errors (client errors) are not retried. // Default: 0 (no retry) // // Example: // // // Retry up to 3 times on network failures // victoria.WithRetry(3) func WithRetry(count int) Option { return func(c *Config) { c.RetryCount = count } } // WithDevMode enables development mode features. // When enabled: // - Handler pings VictoriaLogs endpoint on initialization // - Debug metadata (_handler, _batch) is added to log entries // - May enable more verbose error reporting // // Default: false (disabled) // // Example: // // // Enable during development, disable in production // victoria.WithDevMode(os.Getenv("ENV") == "development") func WithDevMode(enabled bool) Option { return func(c *Config) { c.DevMode = enabled } } golang-github-olekukonko-ll-0.1.8/l3rd/victoria/victoria.go000066400000000000000000000331431516152337300237000ustar00rootroot00000000000000package victoria import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "os" "path" "path/filepath" "strings" "sync" "time" "github.com/olekukonko/ll/lx" ) // Config holds configuration for VictoriaLogs handler. // It contains all settings needed to connect to VictoriaLogs, format log entries, // and control batching behavior. Default values are provided for all fields, // making it easy to use with minimal configuration. // // Example configuration: // // config := &Config{ // URL: "http://localhost:9428", // AppName: "myapp", // Version: "1.0.0", // Environment: "production", // Hostname: "server-01", // StreamKeys: []string{"app", "env", "level", "ns", "host"}, // BatchSize: 100, // BatchWait: time.Second, // } type Config struct { // URL is the VictoriaLogs ingestion endpoint. // // Best practice: // - Set this to the VictoriaLogs base URL, e.g. "http://localhost:9428" // - The handler will append InsertPath automatically. // // Backward compatible: // - If URL already contains "/insert/", it is treated as a full ingestion URL // (e.g. "http://localhost:9428/insert/jsonline") and InsertPath won't be appended. // // Default: "http://localhost:9428" URL string // InsertPath is the ingestion path appended to URL when URL is a base URL. // This lets operators keep secrets/configs stable while the handler chooses // the ingestion format. // // Default: "/insert/jsonline" InsertPath string // AppName identifies the application sending logs. // Default: executable name (without .exe extension) AppName string // Version is the application version for tracking deployments. // Default: "unknown" Version string // Environment specifies the deployment environment. // Common values: "production", "staging", "development", "testing" // Default: "production" Environment string // Hostname identifies the server or pod sending logs. // Default: os.Hostname() result Hostname string // StreamKeys are field names used as stream labels in VictoriaLogs. // Stream labels enable efficient querying and partitioning of logs. // Default: ["app", "env", "level", "ns", "host"] StreamKeys []string // FieldMap provides custom mappings for field names. // Useful for renaming fields to match existing log schemas or conventions. // Example: map["user_id"] = "userId" FieldMap map[string]string // HTTPClient is a custom HTTP client for making requests. // If nil, a default client with reasonable timeouts is created. HTTPClient *http.Client // Timeout is the maximum duration for HTTP requests. // Default: 5 seconds Timeout time.Duration // RetryCount is the number of retry attempts for failed requests. // Default: 0 (no retry) RetryCount int // DevMode enables development features: // - Ping endpoint on initialization // - Add debug metadata to log entries // - More verbose error reporting // Default: false DevMode bool } // Victoria implements lx.Handler for sending logs to VictoriaLogs. // It provides a production-ready logging handler with features like batching, // retries, and configurable stream labeling. The handler is thread-safe and // supports concurrent logging from multiple goroutines. // // Key features: // - JSON Lines (NDJSON) format compatible with VictoriaLogs // - Configurable batching for improved throughput // - Automatic retry with exponential backoff // - Stream labels for efficient querying // - Graceful shutdown with pending log flushing // // Example usage: // // victoriaHandler, err := victoria.New( // victoria.WithURL("http://localhost:9428"), // victoria.WithAppName("myapp"), // ) // if err != nil { // log.Fatal(err) // } // defer victoriaHandler.Close() // // logger := ll.New("app").Enable().Handler(victoriaHandler) // logger.Info("Application started") type Victoria struct { config *Config // Immutable configuration client *http.Client // HTTP client for VictoriaLogs requests mu sync.Mutex // Mutex for thread-safe operations } // New creates and initializes a new VictoriaLogs handler. // It configures the handler with sensible defaults that can be overridden // using Option functions. Returns an error if initialization fails (e.g., // VictoriaLogs endpoint is unreachable in DevMode). // // Parameters: // - opts: Optional configuration functions to customize the handler // // Returns: // - *Victoria: Configured handler ready for use // - error: Non-nil if initialization fails // // Example: // // handler, err := victoria.New( // victoria.WithURL("http://logs.prod.example.com:9428"), // victoria.WithAppName("payment-service"), // victoria.WithEnvironment("production"), // victoria.WithBatching(200, 5*time.Second), // victoria.WithRetry(3), // ) // if err != nil { // return fmt.Errorf("failed to create VictoriaLogs handler: %w", err) // } func New(opts ...Option) (*Victoria, error) { // Get executable name as default app name appName := "unknown" if exe, err := os.Executable(); err == nil { appName = strings.TrimSuffix(filepath.Base(exe), ".exe") } // Get hostname for default configuration hostname, _ := os.Hostname() // Initialize configuration with defaults config := &Config{ URL: "http://localhost:9428", InsertPath: "/insert/jsonline", AppName: appName, Version: "unknown", Environment: "production", Hostname: hostname, StreamKeys: []string{"app", "env", "level", "ns", "host"}, FieldMap: make(map[string]string), Timeout: 5 * time.Second, RetryCount: 0, DevMode: false, } // Apply provided options to override defaults for _, opt := range opts { opt(config) } // Normalize InsertPath (keep config flexible, do not force operators to include "/") if strings.TrimSpace(config.InsertPath) == "" { config.InsertPath = "/insert/jsonline" } if !strings.HasPrefix(config.InsertPath, "/") { config.InsertPath = "/" + config.InsertPath } // Set up HTTP client (use custom or create default) client := config.HTTPClient if client == nil { client = &http.Client{ Timeout: config.Timeout, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 30 * time.Second, }, } } // Create handler instance v := &Victoria{ config: config, client: client, } // Verify connectivity in development mode if config.DevMode { if err := v.Ping(); err != nil { return nil, fmt.Errorf("VictoriaLogs ping failed: %w", err) } } return v, nil } // Handle implements the lx.Handler interface for processing log entries. // It receives log entries from the logger and sends them to VictoriaLogs. // The method supports both immediate sending and batching based on configuration. // Thread-safe: can be called concurrently from multiple goroutines. // // Parameters: // - e: The log entry to process // // Returns: // - error: Non-nil if the entry could not be processed (e.g., buffer full) // // Behavior: // - If batching is enabled: Adds entry to buffer, returns quickly // - If batching is disabled: Sends entry immediately to VictoriaLogs // - If buffer is full: Falls back to immediate sending after timeout func (v *Victoria) Handle(e *lx.Entry) error { return v.sendSingle(e) } // sendSingle sends a single log entry immediately to VictoriaLogs. // This method is used when batching is disabled or when the buffer is full. // It constructs the log line, applies field mappings, and sends with retry logic. // // Parameters: // - e: The log entry to send // // Returns: // - error: Non-nil if the send operation fails after all retries func (v *Victoria) sendSingle(e *lx.Entry) error { line := v.buildLine(e) return v.sendWithRetry(line) } // buildLine constructs a VictoriaLogs-compatible JSON object from a log entry. // It adds standard fields (timestamp, level, message), application metadata, // and any custom fields from the log entry. Field mappings are applied if configured. // // Parameters: // - e: The source log entry // // Returns: // - map[string]interface{}: Formatted log line ready for JSON serialization func (v *Victoria) buildLine(e *lx.Entry) map[string]interface{} { // Base fields required by VictoriaLogs line := map[string]interface{}{ "ts": e.Timestamp.Format(time.RFC3339Nano), // VictoriaLogs expects RFC3339Nano "level": strings.ToLower(e.Level.String()), // Convert to lowercase for consistency "msg": e.Message, "ns": e.Namespace, "app": v.config.AppName, "ver": v.config.Version, "env": v.config.Environment, "host": v.config.Hostname, } // Add custom fields - e.Fields is a slice of key-value pairs for _, field := range e.Fields { key := field.Key value := field.Value // Apply field mapping if configured if mapped, ok := v.config.FieldMap[key]; ok { key = mapped } line[key] = value } // Add stack trace if present (VictoriaLogs can index and search stack traces) if len(e.Stack) > 0 { line["stack"] = string(e.Stack) } // Add debug information in development mode if v.config.DevMode { line["_handler"] = "ll/victoria" } return line } // sendWithRetry sends data to VictoriaLogs with configurable retry logic. // Implements exponential backoff between retries and skips retrying on client // errors (4xx status codes) since they indicate configuration issues. // // Parameters: // - line: The log line to send // // Returns: // - error: The last error encountered, or nil if successful func (v *Victoria) sendWithRetry(line map[string]interface{}) error { var lastErr error for attempt := 0; attempt <= v.config.RetryCount; attempt++ { if attempt > 0 { // Exponential backoff: 0ms, 100ms, 400ms, 900ms... backoff := time.Duration(attempt*attempt*100) * time.Millisecond time.Sleep(backoff) } err := v.send(line) if err == nil { return nil } lastErr = err // Don't retry on 4xx errors (client errors - configuration issues) if strings.Contains(err.Error(), "rejected: 4") || strings.Contains(err.Error(), "status 4") { break } } return lastErr } // endpointURL returns the ingestion URL used by send(). // // Behavior: // - If config.URL already contains "/insert/", it is treated as the full ingestion URL. // - Otherwise, config.InsertPath is appended to config.URL safely. func (v *Victoria) endpointURL() string { raw := strings.TrimSpace(v.config.URL) if raw == "" { raw = "http://localhost:9428" } // Backward-compatible: URL may already be the full ingestion endpoint. if strings.Contains(raw, "/insert/") { return strings.TrimRight(raw, "/") } base := strings.TrimRight(raw, "/") joinedPath := path.Join("/", strings.TrimPrefix(v.config.InsertPath, "/")) return base + joinedPath } // send performs the actual HTTP request to VictoriaLogs. // It serializes the log line to JSON, constructs the VictoriaLogs URL with // query parameters for stream labels, and sends the request. // // Parameters: // - line: The log line to send // // Returns: // - error: Non-nil if the HTTP request fails or VictoriaLogs rejects the log func (v *Victoria) send(line map[string]interface{}) error { // Serialize to JSON b, err := json.Marshal(line) if err != nil { return fmt.Errorf("marshal VictoriaLogs line: %w", err) } // Append newline for NDJSON format data := append(b, '\n') // Build URL with VictoriaLogs query parameters streamFields := strings.Join(v.config.StreamKeys, ",") victoriaURL := fmt.Sprintf("%s?_msg_field=msg&_time_field=ts&_stream_fields=%s", v.endpointURL(), streamFields) // Create request with timeout context ctx, cancel := context.WithTimeout(context.Background(), v.config.Timeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", victoriaURL, bytes.NewReader(data)) if err != nil { return fmt.Errorf("create VictoriaLogs request: %w", err) } req.Header.Set("Content-Type", "application/json") // Execute request resp, err := v.client.Do(req) if err != nil { return fmt.Errorf("VictoriaLogs request failed: %w", err) } defer resp.Body.Close() // Check response status if resp.StatusCode >= 400 { body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) return fmt.Errorf("VictoriaLogs rejected (status %d): %s", resp.StatusCode, string(body)) } return nil } // Ping sends a test request to verify VictoriaLogs connectivity. // This method is automatically called during initialization when DevMode is enabled. // It sends a simple "ping" log entry to ensure the endpoint is reachable and // correctly configured. // // Returns: // - error: Non-nil if the ping fails (connection error or bad response) func (v *Victoria) Ping() error { testData := map[string]interface{}{ "ts": time.Now().Format(time.RFC3339Nano), "level": "info", "msg": "ping from ll/victoria handler", "app": v.config.AppName, "env": v.config.Environment, "host": v.config.Hostname, "_ping": true, } return v.send(testData) } // Close gracefully shuts down the Victoria handler. // It stops accepting new log entries, waits for pending batches to be sent, // and ensures all background goroutines complete. This method should be called // before application exit to prevent log loss. // // Returns: // - error: Always returns nil (errors are logged but not returned) func (v *Victoria) Close() error { return nil } // Timestamped implements the lx.Timestamper interface. // This is a no-op for Victoria handler since timestamps are always included // in VictoriaLogs format (RFC3339Nano). The method exists for interface // compatibility. // // Parameters: // - enable: Ignored (timestamps are always enabled) // - format: Ignored (format is fixed to RFC3339Nano for VictoriaLogs) func (v *Victoria) Timestamped(enable bool, format ...string) { // VictoriaLogs always includes timestamps in RFC3339Nano format // This method exists for lx.Timestamper interface compatibility } golang-github-olekukonko-ll-0.1.8/l3rd/victoria/victoria_test.go000066400000000000000000000340621516152337300247400ustar00rootroot00000000000000package victoria import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" ) // TestNew tests the creation of a new Victoria handler. func TestNew(t *testing.T) { tests := []struct { name string opts []Option wantErr bool check func(*Victoria, *Config) bool }{ { name: "default configuration", opts: []Option{}, wantErr: false, check: func(v *Victoria, c *Config) bool { return c.AppName != "unknown" && c.Environment == "production" && c.Timeout == 5*time.Second && c.DevMode == false }, }, { name: "custom configuration", opts: []Option{ WithAppName("test-app"), WithEnvironment("testing"), WithVersion("1.2.3"), WithHostname("test-host"), WithTimeout(10 * time.Second), WithRetry(3), }, wantErr: false, check: func(v *Victoria, c *Config) bool { return c.AppName == "test-app" && c.Environment == "testing" && c.Version == "1.2.3" && c.Hostname == "test-host" && c.Timeout == 10*time.Second && c.RetryCount == 3 }, }, { name: "field mapping configuration", opts: []Option{ WithFieldMapping("user_id", "userId"), WithFieldMapping("req_id", "requestId"), }, wantErr: false, check: func(v *Victoria, c *Config) bool { return c.FieldMap["user_id"] == "userId" && c.FieldMap["req_id"] == "requestId" }, }, { name: "stream keys configuration", opts: []Option{ WithStreamKeys("app", "env", "level"), }, wantErr: false, check: func(v *Victoria, c *Config) bool { return len(c.StreamKeys) == 3 && c.StreamKeys[0] == "app" && c.StreamKeys[1] == "env" && c.StreamKeys[2] == "level" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) defer server.Close() opts := append(tt.opts, WithURL(server.URL)) v, err := New(opts...) if (err != nil) != tt.wantErr { t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) return } if err == nil { defer v.Close() if !tt.check(v, v.config) { t.Errorf("configuration check failed") } } }) } } // TestHandle tests the Handle method for processing log entries. func TestHandle(t *testing.T) { tests := []struct { name string entry *lx.Entry config []Option expectError bool validate func([]byte) bool }{ { name: "basic info log", entry: &lx.Entry{ Timestamp: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Level: lx.LevelInfo, Message: "test message", Namespace: "test.ns", Fields: nil, }, config: []Option{}, expectError: false, validate: func(data []byte) bool { var line map[string]interface{} if err := json.Unmarshal(data, &line); err != nil { return false } return line["msg"] == "test message" && line["level"] == "info" && line["ns"] == "test.ns" }, }, { name: "error log with stack trace", entry: &lx.Entry{ Timestamp: time.Now(), Level: lx.LevelError, Message: "error occurred", Namespace: "app.error", Stack: []byte("goroutine 1 [running]:\nmain.main()\n\tmain.go:10"), Fields: nil, }, config: []Option{}, expectError: false, validate: func(data []byte) bool { var line map[string]interface{} if err := json.Unmarshal(data, &line); err != nil { return false } return line["level"] == "error" && strings.Contains(line["stack"].(string), "goroutine") }, }, { name: "log with custom fields", entry: &lx.Entry{ Timestamp: time.Now(), Level: lx.LevelWarn, Message: "warning with fields", Namespace: "app.warn", Fields: []lx.Field{ {Key: "user_id", Value: "12345"}, {Key: "requestId", Value: "req-abc"}, {Key: "duration", Value: 150}, }, }, config: []Option{ WithFieldMapping("user_id", "userId"), }, expectError: false, validate: func(data []byte) bool { var line map[string]interface{} if err := json.Unmarshal(data, &line); err != nil { return false } return line["userId"] == "12345" && // Should be mapped line["requestId"] == "req-abc" && line["duration"].(float64) == 150 }, }, { name: "dev mode adds debug info", entry: &lx.Entry{ Timestamp: time.Now(), Level: lx.LevelDebug, Message: "debug message", Namespace: "app.debug", Fields: nil, }, config: []Option{ WithDevMode(true), }, expectError: false, validate: func(data []byte) bool { var line map[string]interface{} if err := json.Unmarshal(data, &line); err != nil { return false } return line["_handler"] == "ll/victoria" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var receivedData []byte var requestCount int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestCount++ data, _ := io.ReadAll(r.Body) receivedData = data w.WriteHeader(http.StatusOK) })) defer server.Close() config := append(tt.config, WithURL(server.URL)) v, err := New(config...) if err != nil { t.Fatalf("failed to create handler: %v", err) } defer v.Close() err = v.Handle(tt.entry) if (err != nil) != tt.expectError { t.Errorf("Handle() error = %v, expectError %v", err, tt.expectError) return } if !tt.expectError && requestCount == 0 { t.Error("no request was made to the test server") return } if requestCount > 0 && tt.validate != nil { if !tt.validate(receivedData) { t.Error("data validation failed") t.Logf("received data: %s", string(receivedData)) } } }) } } // TestRetry tests the retry functionality. func TestRetry(t *testing.T) { tests := []struct { name string retryCount int failTimes int expectError bool }{ { name: "no retry on first failure", retryCount: 0, failTimes: 1, expectError: true, }, { name: "retry succeeds on second attempt", retryCount: 3, failTimes: 1, expectError: false, }, { name: "retry fails after all attempts", retryCount: 2, failTimes: 3, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { attemptCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attemptCount++ if attemptCount <= tt.failTimes { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("server error")) return } w.WriteHeader(http.StatusOK) })) defer server.Close() v, err := New( WithURL(server.URL), WithRetry(tt.retryCount), WithTimeout(100*time.Millisecond), ) if err != nil { t.Fatalf("failed to create handler: %v", err) } defer v.Close() entry := &lx.Entry{ Timestamp: time.Now(), Level: lx.LevelInfo, Message: "test retry", Namespace: "test.retry", Fields: nil, } err = v.Handle(entry) if (err != nil) != tt.expectError { t.Errorf("Handle() error = %v, expectError %v", err, tt.expectError) } expectedAttempts := tt.failTimes + 1 if !tt.expectError && attemptCount != expectedAttempts { t.Errorf("expected %d attempts, got %d", expectedAttempts, attemptCount) } }) } } // TestPing tests the Ping method for connectivity verification. func TestPing(t *testing.T) { tests := []struct { name string serverFunc http.HandlerFunc devMode bool expectError bool }{ { name: "successful ping", serverFunc: func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }, devMode: true, expectError: false, }, { name: "ping fails with server error", serverFunc: func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) }, devMode: true, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(tt.serverFunc)) defer server.Close() v, err := New( WithURL(server.URL), WithDevMode(tt.devMode), WithTimeout(100*time.Millisecond), ) if tt.devMode { if (err != nil) != tt.expectError { t.Errorf("New() error = %v, expectError %v", err, tt.expectError) } } else { if err != nil { t.Fatalf("failed to create handler: %v", err) } defer v.Close() err = v.Ping() if (err != nil) != tt.expectError { t.Errorf("Ping() error = %v, expectError %v", err, tt.expectError) } } }) } } // TestClose tests the graceful shutdown functionality. func TestClose(t *testing.T) { tests := []struct { name string numLogs int expectFlushed int }{ { name: "close with no pending logs", numLogs: 0, expectFlushed: 0, }, { name: "close without batching", numLogs: 3, expectFlushed: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var flushedLogs []string var mu sync.Mutex server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data, _ := io.ReadAll(r.Body) mu.Lock() lines := strings.Split(strings.TrimSpace(string(data)), "\n") for _, line := range lines { if line != "" { var logEntry map[string]interface{} if err := json.Unmarshal([]byte(line), &logEntry); err == nil { flushedLogs = append(flushedLogs, logEntry["msg"].(string)) } } } mu.Unlock() w.WriteHeader(http.StatusOK) })) defer server.Close() v, err := New( WithURL(server.URL), ) if err != nil { t.Fatalf("failed to create handler: %v", err) } for i := 0; i < tt.numLogs; i++ { entry := &lx.Entry{ Timestamp: time.Now(), Level: lx.LevelInfo, Message: fmt.Sprintf("log %d", i), Namespace: "test.close", Fields: nil, } if err := v.Handle(entry); err != nil { t.Errorf("Handle() error = %v", err) } } if err := v.Close(); err != nil { t.Errorf("Close() error = %v", err) } mu.Lock() actualFlushed := len(flushedLogs) mu.Unlock() if actualFlushed != tt.expectFlushed { t.Errorf("expected %d logs flushed, got %d", tt.expectFlushed, actualFlushed) } }) } } // TestConcurrentHandling tests thread-safe concurrent logging. func TestConcurrentHandling(t *testing.T) { const numGoroutines = 10 const logsPerGoroutine = 100 var receivedLogs []string var mu sync.Mutex totalExpected := numGoroutines * logsPerGoroutine server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data, _ := io.ReadAll(r.Body) mu.Lock() lines := strings.Split(strings.TrimSpace(string(data)), "\n") for _, line := range lines { if line != "" { var logEntry map[string]interface{} if err := json.Unmarshal([]byte(line), &logEntry); err == nil { receivedLogs = append(receivedLogs, logEntry["msg"].(string)) } } } mu.Unlock() w.WriteHeader(http.StatusOK) })) defer server.Close() v, err := New( WithURL(server.URL), ) if err != nil { t.Fatalf("failed to create handler: %v", err) } defer v.Close() var wg sync.WaitGroup for i := 0; i < numGoroutines; i++ { wg.Add(1) go func(goroutineID int) { defer wg.Done() for j := 0; j < logsPerGoroutine; j++ { entry := &lx.Entry{ Timestamp: time.Now(), Level: lx.LevelInfo, Message: fmt.Sprintf("goroutine-%d-log-%d", goroutineID, j), Namespace: "test.concurrent", Fields: nil, } if err := v.Handle(entry); err != nil { t.Errorf("Handle() error from goroutine %d: %v", goroutineID, err) } } }(i) } wg.Wait() if err := v.Close(); err != nil { t.Errorf("Close() error: %v", err) } mu.Lock() actualCount := len(receivedLogs) mu.Unlock() if actualCount != totalExpected { t.Errorf("expected %d logs, received %d", totalExpected, actualCount) } } // TestRaceCondition reproduces the data race where the logger reuses // an entry while the buffered victoria handler is still processing it. func TestRaceCondition(t *testing.T) { var requestCount int var mu sync.Mutex server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() requestCount++ mu.Unlock() // Add delay to increase chance of race time.Sleep(10 * time.Millisecond) io.ReadAll(r.Body) w.WriteHeader(http.StatusOK) })) defer server.Close() v, err := New( WithURL(server.URL), WithTimeout(5*time.Second), ) if err != nil { t.Fatalf("failed to create handler: %v", err) } defer v.Close() // Wrap in buffered handler to simulate the race condition buffered := lh.NewBuffered(v, lh.WithBatchSize(10), lh.WithFlushInterval(100*time.Millisecond), lh.WithMaxBuffer(100), ) defer buffered.Close() // Simulate what happens in ll: entries are pooled and reused entryPool := &sync.Pool{ New: func() interface{} { return &lx.Entry{ Fields: make([]lx.Field, 0, 8), } }, } var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func(id int) { defer wg.Done() for j := 0; j < 10; j++ { // Get entry from pool (simulating logger's behavior) e := entryPool.Get().(*lx.Entry) e.Timestamp = time.Now() e.Level = lx.LevelInfo e.Message = "test message" e.Namespace = "test.race" e.Fields = e.Fields[:0] e.Fields = append(e.Fields, lx.Field{Key: "id", Value: id}) e.Fields = append(e.Fields, lx.Field{Key: "seq", Value: j}) // Send to buffered handler buffered.Handle(e) // Immediately reset and return to pool (this causes the race) // The logger does this via defer in log() e.Timestamp = time.Time{} e.Level = 0 e.Message = "" e.Namespace = "" e.Fields = e.Fields[:0] entryPool.Put(e) } }(i) } wg.Wait() buffered.Flush() mu.Lock() count := requestCount mu.Unlock() t.Logf("Processed %d requests", count) } golang-github-olekukonko-ll-0.1.8/lc.go000066400000000000000000000036421516152337300177730ustar00rootroot00000000000000package 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.1.8/lh/000077500000000000000000000000001516152337300174445ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/lh/buffered.go000066400000000000000000000222671516152337300215660ustar00rootroot00000000000000package 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) FlushTimeout time.Duration // FlushTimeout specifies the duration to wait for a flush attempt to complete before timing out. MaxBuffer int // Maximum buffer size before applying backpressure (default: 1000) OnOverflow func(int) // Called when buffer reaches MaxBuffer (default: logs warning) ErrorOutput io.Writer // Destination for internal errors like flush failures (default: os.Stderr) } // BufferingOpt configures Buffered handler. type BufferingOpt func(*Buffering) // WithBatchSize sets the batch size for flushing. func WithBatchSize(size int) BufferingOpt { return func(c *Buffering) { c.BatchSize = size } } // WithFlushInterval sets the maximum time between flushes. func WithFlushInterval(d time.Duration) BufferingOpt { return func(c *Buffering) { c.FlushInterval = d } } // WithFlushTimeout sets the maximum time to wait for a flush to complete. func WithFlushTimeout(d time.Duration) BufferingOpt { return func(c *Buffering) { c.FlushTimeout = d } } // WithMaxBuffer sets the maximum buffer size before backpressure. func WithMaxBuffer(size int) BufferingOpt { return func(c *Buffering) { c.MaxBuffer = size } } // WithOverflowHandler sets the overflow callback. func WithOverflowHandler(fn func(int)) BufferingOpt { return func(c *Buffering) { c.OnOverflow = fn } } // WithErrorOutput sets the destination for internal errors (e.g., downstream handler failures). func WithErrorOutput(w io.Writer) BufferingOpt { return func(c *Buffering) { c.ErrorOutput = w } } // batchHandler is an optional interface that handlers may implement to receive // an entire flush batch in a single call instead of one entry at a time. // When implemented, flushBatch calls HandleBatch once per batch, allowing // the handler (and test mocks) to track flush operations rather than // individual per-entry Handle calls. type batchHandler interface { HandleBatch(entries []*lx.Entry) error } // 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 config *Buffering entries chan *lx.Entry flushSignal chan struct{} shutdown chan struct{} shutdownOnce sync.Once wg sync.WaitGroup } // 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] { config := &Buffering{ BatchSize: 100, FlushInterval: 10 * time.Second, MaxBuffer: 1000, ErrorOutput: os.Stderr, OnOverflow: func(count int) { fmt.Fprintf(io.Discard, "log buffer overflow: %d entries\n", count) }, } for _, opt := range opts { opt(config) } if config.BatchSize < 1 { config.BatchSize = 1 } // Ensure the channel always holds at least BatchSize entries so a single // batch can always be enqueued without blocking. if config.MaxBuffer < config.BatchSize { config.MaxBuffer = config.BatchSize * 10 } if config.FlushInterval <= 0 { config.FlushInterval = 10 * time.Second } if config.FlushTimeout <= 0 { config.FlushTimeout = 100 * time.Millisecond } if config.ErrorOutput == nil { config.ErrorOutput = os.Stderr } b := &Buffered[H]{ handler: handler, config: config, entries: make(chan *lx.Entry, config.MaxBuffer), flushSignal: make(chan struct{}, 1), shutdown: make(chan struct{}), } b.wg.Add(1) go b.worker() runtime.SetFinalizer(b, (*Buffered[H]).Final) return b } // cloneEntry creates a deep copy of an entry for safe asynchronous processing. // The original entry belongs to the logger's pool and is reused immediately after Handle() returns. func (b *Buffered[H]) cloneEntry(e *lx.Entry) *lx.Entry { entryCopy := &lx.Entry{ Timestamp: e.Timestamp, Level: e.Level, Message: e.Message, Namespace: e.Namespace, Style: e.Style, Class: e.Class, Error: e.Error, Id: e.Id, } if len(e.Fields) > 0 { entryCopy.Fields = make(lx.Fields, len(e.Fields)) copy(entryCopy.Fields, e.Fields) } if len(e.Stack) > 0 { entryCopy.Stack = make([]byte, len(e.Stack)) copy(entryCopy.Stack, e.Stack) } return entryCopy } // Handle implements the lx.Handler interface. func (b *Buffered[H]) Handle(e *lx.Entry) error { entryCopy := b.cloneEntry(e) select { case b.entries <- entryCopy: return nil default: if b.config.OnOverflow != nil { b.config.OnOverflow(len(b.entries)) } return fmt.Errorf("log buffer overflow") } } // Flush triggers an immediate flush of 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{}{}: case <-time.After(b.config.FlushTimeout): } } // Close flushes any remaining entries and stops the worker. // It ensures shutdown is performed only once and waits for the worker to finish. // If the underlying handler implements a Close() error method, it will be called to release resources. // Thread-safe via sync.Once and WaitGroup. // Returns any error from the underlying handler's Close, or nil. // Example: // // buffered.Close() // Flushes entries and stops worker func (b *Buffered[H]) Close() error { var closeErr error b.shutdownOnce.Do(func() { close(b.shutdown) b.wg.Wait() runtime.SetFinalizer(b, nil) if closer, ok := any(b.handler).(interface{ Close() error }); ok { closeErr = closer.Close() } }) return closeErr } // Final ensures remaining entries are flushed during garbage collection. func (b *Buffered[H]) Final() { b.Close() } // Config returns the current configuration of the Buffered handler. func (b *Buffered[H]) Config() *Buffering { return b.config } // worker processes entries and handles flushing. func (b *Buffered[H]) worker() { defer b.wg.Done() batch := make([]*lx.Entry, 0, b.config.BatchSize) ticker := time.NewTicker(b.config.FlushInterval) defer ticker.Stop() for { select { case entry := <-b.entries: batch = append(batch, entry) if len(batch) >= b.config.BatchSize { b.flushBatch(batch) batch = batch[:0] ticker.Reset(b.config.FlushInterval) } case <-ticker.C: if len(batch) > 0 { b.flushBatch(batch) batch = batch[:0] } case <-b.flushSignal: if len(batch) > 0 { b.flushBatch(batch) batch = batch[:0] } b.drainRemaining() ticker.Reset(b.config.FlushInterval) case <-b.shutdown: // Merge whatever is already in batch with anything remaining in // the channel, then flush everything in a single call so that // callCount increments exactly once regardless of how many // entries the worker happened to have pre-loaded into batch. batch = b.collectRemaining(batch) if len(batch) > 0 { b.flushBatch(batch) } return } } } // flushBatch processes a batch of entries through the wrapped handler. // If the handler implements the batchHandler interface, the entire batch is // delivered in a single HandleBatch call (one "flush event"). Otherwise each // entry is forwarded individually via Handle. func (b *Buffered[H]) flushBatch(batch []*lx.Entry) { if bh, ok := any(b.handler).(batchHandler); ok { if err := bh.HandleBatch(batch); err != nil { if b.config.ErrorOutput != nil { fmt.Fprintf(b.config.ErrorOutput, "log flush error: %v\n", err) } } return } for _, entry := range batch { if err := b.handler.Handle(entry); err != nil { if b.config.ErrorOutput != nil { fmt.Fprintf(b.config.ErrorOutput, "log flush error: %v\n", err) } } } } // collectRemaining drains the entries channel into the provided slice and // returns the extended slice without flushing, so the caller can flush // everything atomically in a single batch. func (b *Buffered[H]) collectRemaining(batch []*lx.Entry) []*lx.Entry { for { select { case entry := <-b.entries: batch = append(batch, entry) default: return batch } } } // drainRemaining processes any remaining entries in the channel. // Collects entries into a batch and flushes them together for efficiency. func (b *Buffered[H]) drainRemaining() { batch := make([]*lx.Entry, 0, b.config.BatchSize) for { select { case entry := <-b.entries: batch = append(batch, entry) if len(batch) >= b.config.BatchSize { b.flushBatch(batch) batch = batch[:0] } default: if len(batch) > 0 { b.flushBatch(batch) } return } } } golang-github-olekukonko-ll-0.1.8/lh/buffered_test.go000066400000000000000000000414221516152337300226170ustar00rootroot00000000000000package lh import ( "bytes" "errors" "fmt" "io" "os" "strings" "sync" "sync/atomic" "testing" "time" "github.com/olekukonko/ll/lx" ) // mockHandlerBuffered is a test handler that tracks flush-batch calls and errors. // It implements the batchHandler interface so flushBatch delivers the entire // batch in one HandleBatch call; callCount therefore tracks the number of flush // operations (batches) rather than the number of individual log entries. type mockHandlerBuffered struct { mu sync.Mutex entries []*lx.Entry callCount int32 err error delay time.Duration } // Handle processes a single entry without incrementing callCount. // Because mockHandlerBuffered implements batchHandler, flushBatch always routes // through HandleBatch instead of Handle. This method satisfies lx.Handler and // supports direct single-entry calls used by code paths that bypass flushBatch. func (m *mockHandlerBuffered) Handle(e *lx.Entry) error { if m.delay > 0 { time.Sleep(m.delay) } m.mu.Lock() m.entries = append(m.entries, e) m.mu.Unlock() return m.err } // HandleBatch implements batchHandler. Called by flushBatch once per flush // operation so callCount accurately tracks "number of flush batches". func (m *mockHandlerBuffered) HandleBatch(entries []*lx.Entry) error { if m.delay > 0 { time.Sleep(m.delay) } atomic.AddInt32(&m.callCount, 1) m.mu.Lock() m.entries = append(m.entries, entries...) m.mu.Unlock() return m.err } func (m *mockHandlerBuffered) Entries() []*lx.Entry { m.mu.Lock() defer m.mu.Unlock() out := make([]*lx.Entry, len(m.entries)) copy(out, m.entries) return out } func (m *mockHandlerBuffered) CallCount() int32 { return atomic.LoadInt32(&m.callCount) } // TestNewBuffered_Basic tests basic creation func TestNewBuffered_Basic(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler) if b == nil { t.Fatal("expected buffered handler to be created") } if b.config.BatchSize != 100 { t.Fatalf("expected default batch size 100, got %d", b.config.BatchSize) } if b.config.FlushInterval != 10*time.Second { t.Fatalf("expected default flush interval 10s, got %v", b.config.FlushInterval) } b.Close() } // TestNewBuffered_CustomConfig tests custom configuration func TestNewBuffered_CustomConfig(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler, WithBatchSize(50), WithFlushInterval(5*time.Second), WithMaxBuffer(500), WithFlushTimeout(200*time.Millisecond), ) if b.config.BatchSize != 50 { t.Fatalf("expected batch size 50, got %d", b.config.BatchSize) } if b.config.FlushInterval != 5*time.Second { t.Fatalf("expected flush interval 5s, got %v", b.config.FlushInterval) } if b.config.MaxBuffer != 500 { t.Fatalf("expected max buffer 500, got %d", b.config.MaxBuffer) } if b.config.FlushTimeout != 200*time.Millisecond { t.Fatalf("expected flush timeout 200ms, got %v", b.config.FlushTimeout) } b.Close() } // TestNewBuffered_InvalidConfig tests config validation func TestNewBuffered_InvalidConfig(t *testing.T) { handler := &mockHandlerBuffered{} // BatchSize < 1 should be set to 1 b := NewBuffered(handler, WithBatchSize(0)) if b.config.BatchSize != 1 { t.Fatalf("expected batch size 1, got %d", b.config.BatchSize) } b.Close() // MaxBuffer < BatchSize should be raised to BatchSize * 10 b = NewBuffered(handler, WithBatchSize(100), WithMaxBuffer(50)) if b.config.MaxBuffer != 1000 { t.Fatalf("expected max buffer 1000, got %d", b.config.MaxBuffer) } b.Close() // FlushInterval <= 0 should default to 10s b = NewBuffered(handler, WithFlushInterval(0)) if b.config.FlushInterval != 10*time.Second { t.Fatalf("expected flush interval 10s, got %v", b.config.FlushInterval) } b.Close() // FlushTimeout <= 0 should default to 100ms b = NewBuffered(handler, WithFlushTimeout(0)) if b.config.FlushTimeout != 100*time.Millisecond { t.Fatalf("expected flush timeout 100ms, got %v", b.config.FlushTimeout) } b.Close() } // TestBuffered_Handle_Basic tests basic entry handling func TestBuffered_Handle_Basic(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler, WithBatchSize(10)) entry := &lx.Entry{ Level: lx.LevelInfo, Message: "test message", } err := b.Handle(entry) if err != nil { t.Fatalf("Handle failed: %v", err) } // Entry should be buffered, not yet flushed if handler.CallCount() != 0 { t.Fatal("handler should not have been called yet") } b.Close() // After close, should be flushed if handler.CallCount() != 1 { t.Fatalf("expected 1 call, got %d", handler.CallCount()) } } // TestBuffered_Handle_BatchFlush tests batch size triggering flush func TestBuffered_Handle_BatchFlush(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler, WithBatchSize(5)) // Send 4 entries - should not flush yet for i := 0; i < 4; i++ { b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: fmt.Sprintf("msg %d", i)}) } if handler.CallCount() != 0 { t.Fatal("should not have flushed yet") } // 5th entry triggers flush b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "trigger"}) // Give time for async flush time.Sleep(50 * time.Millisecond) if handler.CallCount() != 1 { t.Fatalf("expected 1 flush, got %d", handler.CallCount()) } entries := handler.Entries() if len(entries) != 5 { t.Fatalf("expected 5 entries, got %d", len(entries)) } b.Close() } // TestBuffered_TimerFlush tests timer-based flushing func TestBuffered_TimerFlush(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler, WithBatchSize(100), // Large batch size WithFlushInterval(100*time.Millisecond), // Short interval ) b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "test"}) // Should not flush immediately if handler.CallCount() != 0 { t.Fatal("should not have flushed immediately") } // Wait for timer time.Sleep(150 * time.Millisecond) if handler.CallCount() != 1 { t.Fatalf("expected timer flush, got %d calls", handler.CallCount()) } b.Close() } // TestBuffered_ExplicitFlush tests explicit Flush() call func TestBuffered_ExplicitFlush(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler, WithBatchSize(100)) b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "test"}) b.Flush() // Give time for async flush time.Sleep(50 * time.Millisecond) if handler.CallCount() != 1 { t.Fatalf("expected flush after Flush(), got %d calls", handler.CallCount()) } b.Close() } // TestBuffered_FlushSignalResetsTicker tests that explicit flush resets the ticker // This catches the goroutine leak bug where ticker wasn't reset func TestBuffered_FlushSignalResetsTicker(t *testing.T) { handler := &mockHandlerBuffered{} flushInterval := 200 * time.Millisecond b := NewBuffered(handler, WithBatchSize(100), // Never flush on size WithFlushInterval(flushInterval), ) // Send entry and flush immediately - resets ticker b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "first"}) b.Flush() time.Sleep(50 * time.Millisecond) // Wait for flush // Send another entry b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "second"}) // If ticker wasn't reset, it would fire at ~200ms from start // If reset, it fires at ~200ms from flush (~250ms from start) // We check at 150ms - should NOT have flushed yet if reset time.Sleep(100 * time.Millisecond) callCount := handler.CallCount() if callCount != 1 { t.Fatalf("ticker not reset - expected 1 call at 150ms, got %d", callCount) } // Wait for timer flush time.Sleep(150 * time.Millisecond) if handler.CallCount() != 2 { t.Fatalf("expected timer flush, got %d calls", handler.CallCount()) } b.Close() } // blockingBatchHandler is a test handler whose HandleBatch blocks until the // caller releases it via the gate channel. This lets the test pin the worker // goroutine inside HandleBatch so the entries channel stays full. type blockingBatchHandler struct { mu sync.Mutex entries []*lx.Entry gate chan struct{} // close to unblock all waiting HandleBatch calls } func (h *blockingBatchHandler) Handle(e *lx.Entry) error { h.mu.Lock() h.entries = append(h.entries, e) h.mu.Unlock() return nil } func (h *blockingBatchHandler) HandleBatch(entries []*lx.Entry) error { <-h.gate // block until released h.mu.Lock() h.entries = append(h.entries, entries...) h.mu.Unlock() return nil } // TestBuffered_Overflow tests buffer overflow handling. // // Strategy: use a blockingBatchHandler whose HandleBatch blocks on a gate // channel. We send one entry and flush so the worker goroutine enters // HandleBatch and stays there. While the worker is blocked it cannot read // more entries from the channel, so we can reliably fill it to capacity and // then confirm the next Handle call returns an overflow error. func TestBuffered_Overflow(t *testing.T) { gate := make(chan struct{}) handler := &blockingBatchHandler{gate: gate} overflowCalled := int32(0) b := NewBuffered(handler, WithMaxBuffer(5), WithBatchSize(100), WithFlushInterval(time.Hour), // never auto-flush WithOverflowHandler(func(n int) { atomic.AddInt32(&overflowCalled, 1) }), ) maxBuffer := b.Config().MaxBuffer // actual channel capacity after floor // Seed one entry and flush so the worker goroutine calls HandleBatch and // blocks on the gate, leaving it unable to drain the channel further. b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "seed"}) b.Flush() // Give the worker time to enter HandleBatch and block on the gate. time.Sleep(20 * time.Millisecond) // Fill the channel to capacity. The worker is stuck, so every slot stays // occupied. We have already used 1 slot for the seed entry that the worker // has dequeued, so we can enqueue maxBuffer entries. for i := 0; i < maxBuffer; i++ { err := b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: fmt.Sprintf("fill %d", i)}) if err != nil { // Unblock the worker before failing so Close() can complete. close(gate) t.Fatalf("unexpected error filling slot %d: %v", i, err) } } // Channel is now full; the next Handle must overflow. err := b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "overflow"}) // Unblock the worker so Close() can drain and finish cleanly. close(gate) if err == nil { t.Fatal("expected overflow error") } if atomic.LoadInt32(&overflowCalled) == 0 { t.Fatal("overflow handler should have been called") } b.Close() } // TestBuffered_OverflowWithFlushTrigger tests overflow triggering flush. // WithMaxBuffer(3) + WithBatchSize(100) triggers the floor: MaxBuffer becomes // 1000. Filling only 3 slots and then adding a 4th always succeeds because // the channel is nowhere near full. func TestBuffered_OverflowWithFlushTrigger(t *testing.T) { handler := &mockHandlerBuffered{} handler.delay = 10 * time.Millisecond b := NewBuffered(handler, WithMaxBuffer(3), WithBatchSize(100), ) // Fill a few slots (well within channel capacity after floor adjustment) for i := 0; i < 3; i++ { b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: fmt.Sprintf("fill %d", i)}) } // Should succeed: channel has plenty of room err := b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "should succeed"}) if err != nil { t.Fatalf("expected success after flush trigger: %v", err) } b.Close() } // TestBuffered_HandlerError tests error handling from underlying handler func TestBuffered_HandlerError(t *testing.T) { handler := &mockHandlerBuffered{} handler.err = errors.New("handler failed") var errBuf bytes.Buffer b := NewBuffered(handler, WithBatchSize(1), WithErrorOutput(&errBuf), ) b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "test"}) // Give time for async processing time.Sleep(50 * time.Millisecond) b.Close() errOutput := errBuf.String() if !strings.Contains(errOutput, "handler failed") { t.Fatalf("expected error in output, got: %s", errOutput) } } // TestBuffered_CloseFlushesRemaining tests that Close flushes pending entries func TestBuffered_CloseFlushesRemaining(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler, WithBatchSize(100)) // Send entries without triggering flush for i := 0; i < 10; i++ { b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: fmt.Sprintf("msg %d", i)}) } if handler.CallCount() != 0 { t.Fatal("should not have flushed yet") } b.Close() if handler.CallCount() != 1 { t.Fatalf("expected flush on close, got %d calls", handler.CallCount()) } entries := handler.Entries() if len(entries) != 10 { t.Fatalf("expected 10 entries, got %d", len(entries)) } } // TestBuffered_DoubleClose tests that double close is safe func TestBuffered_CloseIdempotent(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler) b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "test"}) err1 := b.Close() err2 := b.Close() if err1 != nil { t.Fatalf("first close failed: %v", err1) } // Second close should not panic, error is handler-dependent _ = err2 if handler.CallCount() != 1 { t.Fatalf("expected 1 call, got %d", handler.CallCount()) } } // TestBuffered_HandlerClose tests that underlying handler is closed func TestBuffered_HandlerClose(t *testing.T) { closeHandler := &closeableHandler{} b := NewBuffered(closeHandler) b.Close() if !closeHandler.closed { t.Fatal("underlying handler should have been closed") } } type closeableHandler struct { closed bool mu sync.Mutex } func (c *closeableHandler) Handle(e *lx.Entry) error { return nil } func (c *closeableHandler) Close() error { c.mu.Lock() c.closed = true c.mu.Unlock() return nil } // TestBuffered_ConcurrentAccess tests thread safety func TestBuffered_ConcurrentAccess(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler, WithBatchSize(10)) var wg sync.WaitGroup numGoroutines := 50 entriesPerGoroutine := 20 for i := 0; i < numGoroutines; i++ { wg.Add(1) go func(id int) { defer wg.Done() for j := 0; j < entriesPerGoroutine; j++ { entry := &lx.Entry{ Level: lx.LevelInfo, Message: fmt.Sprintf("g%d-e%d", id, j), } if err := b.Handle(entry); err != nil { t.Errorf("Handle failed: %v", err) } } }(i) } wg.Wait() b.Close() totalEntries := numGoroutines * entriesPerGoroutine entries := handler.Entries() if len(entries) != totalEntries { t.Fatalf("expected %d entries, got %d", totalEntries, len(entries)) } } // TestBuffered_CloneEntry tests that entries are properly cloned func TestBuffered_CloneEntry(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler, WithBatchSize(1)) original := &lx.Entry{ Level: lx.LevelInfo, Message: "original", Namespace: "test", Fields: lx.Fields{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}, Stack: []byte("stack trace"), } b.Handle(original) // Modify original original.Message = "modified" original.Fields[0].Value = "modified" original.Stack[0] = 'X' time.Sleep(50 * time.Millisecond) b.Close() entries := handler.Entries() if len(entries) != 1 { t.Fatalf("expected 1 entry, got %d", len(entries)) } cloned := entries[0] if cloned.Message != "original" { t.Fatal("entry was not cloned - message modified") } if cloned.Fields[0].Value != "value1" { t.Fatal("entry was not cloned - fields modified") } if string(cloned.Stack) != "stack trace" { t.Fatal("entry was not cloned - stack modified") } } // TestBuffered_Config returns correct config func TestBuffered_Config(t *testing.T) { handler := &mockHandlerBuffered{} b := NewBuffered(handler, WithBatchSize(42)) cfg := b.Config() if cfg.BatchSize != 42 { t.Fatalf("expected batch size 42, got %d", cfg.BatchSize) } b.Close() } // TestBuffered_DefaultErrorOutput tests default error output func TestBuffered_DefaultErrorOutput(t *testing.T) { // Capture stderr oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stderr = w handler := &mockHandlerBuffered{} handler.err = errors.New("test error") b := NewBuffered(handler, WithBatchSize(1), WithErrorOutput(nil)) b.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "test"}) time.Sleep(50 * time.Millisecond) b.Close() w.Close() os.Stderr = oldStderr var buf bytes.Buffer io.Copy(&buf, r) // Should have written to stderr if buf.Len() == 0 { t.Fatal("expected error output to stderr") } } // BenchmarkBuffered_Handle benchmarks entry handling func BenchmarkBuffered_Handle(b *testing.B) { handler := &mockHandlerBuffered{} buf := NewBuffered(handler, WithBatchSize(1000), WithMaxBuffer(10000)) entry := &lx.Entry{ Level: lx.LevelInfo, Message: "benchmark", } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { buf.Handle(entry) } }) buf.Close() } // BenchmarkBuffered_Flush benchmarks flush performance func BenchmarkBuffered_Flush(b *testing.B) { for i := 0; i < b.N; i++ { handler := &mockHandlerBuffered{} buf := NewBuffered(handler, WithBatchSize(100)) for j := 0; j < 100; j++ { buf.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "test"}) } buf.Close() } } golang-github-olekukonko-ll-0.1.8/lh/colorized.go000066400000000000000000000703211516152337300217700ustar00rootroot00000000000000package lh import ( "bytes" "fmt" "hash/fnv" "io" "os" "strconv" "strings" "sync" "time" "github.com/olekukonko/ll/lx" ) // ColorIntensity defines the intensity level for ANSI colors type ColorIntensity int const ( IntensityNormal ColorIntensity = iota IntensityBright IntensityPastel IntensityVibrant ) // Palette defines ANSI color codes for various log components. 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 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 Fatal string // Color for Fatal level messages Title string // Color for dump titles (BEGIN/END separators) // Field type colors Key string // Color for field keys Number string // Color for numbers String string // Color for strings Bool string // Color for booleans Time string // Color for timestamps/durations Nil string // Color for nil values Default string // Default color for unknown types // JSON and Inspect specific colors JSONKey string // Color for JSON keys JSONString string // Color for JSON string values JSONNumber string // Color for JSON number values JSONBool string // Color for JSON boolean values JSONNull string // Color for JSON null values JSONBrace string // Color for JSON braces and brackets InspectKey string // Color for inspect keys InspectValue string // Color for inspect values InspectMeta string // Color for inspect metadata (annotations) } // darkPalette defines colors optimized for dark terminal backgrounds. var darkPalette = Palette{ Header: "\033[1;38;5;203m", // Brighter red Goroutine: "\033[1;38;5;51m", // Bright cyan Func: "\033[1;97m", // Bright white Path: "\033[38;5;110m", // Brighter gray-blue FileLine: "\033[38;5;117m", // Bright blue Reset: "\033[0m", Title: "\033[38;5;245m", Pos: "\033[38;5;117m", Hex: "\033[38;5;156m", Ascii: "\033[38;5;224m", Debug: "\033[36m", Info: "\033[32m", Warn: "\033[33m", Error: "\033[31m", Fatal: "\033[1;31m", // Field type colors - made brighter for dark backgrounds Key: "\033[38;5;117m", // Brighter blue Number: "\033[38;5;141m", // Brighter purple String: "\033[38;5;223m", // Brighter yellow/orange Bool: "\033[38;5;85m", // Brighter green Time: "\033[38;5;110m", // Brighter cyan-blue Nil: "\033[38;5;243m", // Slightly brighter gray Default: "\033[38;5;250m", // Brighter gray // JSON and Inspect colors JSONKey: "\033[38;5;117m", JSONString: "\033[38;5;223m", JSONNumber: "\033[38;5;141m", JSONBool: "\033[38;5;85m", JSONNull: "\033[38;5;243m", JSONBrace: "\033[38;5;245m", InspectKey: "\033[38;5;117m", InspectValue: "\033[38;5;223m", InspectMeta: "\033[38;5;243m", } // lightPalette defines colors optimized for light terminal backgrounds. var lightPalette = Palette{ Header: "\033[1;31m", Goroutine: "\033[34m", Func: "\033[30m", Path: "\033[90m", FileLine: "\033[94m", Reset: "\033[0m", Title: "\033[38;5;245m", Pos: "\033[38;5;117m", Hex: "\033[38;5;156m", Ascii: "\033[38;5;224m", Debug: "\033[36m", Info: "\033[32m", Warn: "\033[33m", Error: "\033[31m", Fatal: "\033[1;31m", Key: "\033[34m", Number: "\033[35m", String: "\033[38;5;94m", Bool: "\033[32m", Time: "\033[38;5;24m", Nil: "\033[38;5;240m", Default: "\033[30m", JSONKey: "\033[1;34m", JSONString: "\033[1;33m", JSONNumber: "\033[1;35m", JSONBool: "\033[1;32m", JSONNull: "\033[1;37m", JSONBrace: "\033[1;37m", InspectKey: "\033[1;34m", InspectValue: "\033[1;33m", InspectMeta: "\033[1;37m", } // brightPalette defines vibrant, high-contrast colors var brightPalette = Palette{ Header: "\033[1;91m", Goroutine: "\033[1;96m", Func: "\033[1;97m", Path: "\033[38;5;250m", FileLine: "\033[38;5;117m", Reset: "\033[0m", Title: "\033[1;37m", Pos: "\033[1;33m", Hex: "\033[1;32m", Ascii: "\033[1;35m", Debug: "\033[1;36m", Info: "\033[1;32m", Warn: "\033[1;33m", Error: "\033[1;31m", Fatal: "\033[1;91m", Key: "\033[1;34m", Number: "\033[1;35m", String: "\033[1;33m", Bool: "\033[1;32m", Time: "\033[1;36m", Nil: "\033[1;37m", Default: "\033[1;37m", JSONKey: "\033[1;34m", JSONString: "\033[1;33m", JSONNumber: "\033[1;35m", JSONBool: "\033[1;32m", JSONNull: "\033[1;37m", JSONBrace: "\033[1;37m", InspectKey: "\033[1;34m", InspectValue: "\033[1;33m", InspectMeta: "\033[1;37m", } // pastelPalette defines soft, pastel colors var pastelPalette = Palette{ Header: "\033[38;5;211m", Goroutine: "\033[38;5;153m", Func: "\033[38;5;255m", Path: "\033[38;5;248m", FileLine: "\033[38;5;111m", Reset: "\033[0m", Title: "\033[38;5;248m", Pos: "\033[38;5;153m", Hex: "\033[38;5;158m", Ascii: "\033[38;5;218m", Debug: "\033[38;5;122m", Info: "\033[38;5;120m", Warn: "\033[38;5;221m", Error: "\033[38;5;211m", Fatal: "\033[38;5;204m", Key: "\033[38;5;153m", Number: "\033[38;5;183m", String: "\033[38;5;223m", Bool: "\033[38;5;120m", Time: "\033[38;5;117m", Nil: "\033[38;5;247m", Default: "\033[38;5;250m", JSONKey: "\033[38;5;153m", JSONString: "\033[38;5;223m", JSONNumber: "\033[38;5;183m", JSONBool: "\033[38;5;120m", JSONNull: "\033[38;5;247m", JSONBrace: "\033[38;5;247m", InspectKey: "\033[38;5;153m", InspectValue: "\033[38;5;223m", InspectMeta: "\033[38;5;247m", } // vibrantPalette defines highly saturated, eye-catching colors var vibrantPalette = Palette{ Header: "\033[38;5;196m", Goroutine: "\033[38;5;51m", Func: "\033[38;5;15m", Path: "\033[38;5;244m", FileLine: "\033[38;5;75m", Reset: "\033[0m", Title: "\033[38;5;244m", Pos: "\033[38;5;51m", Hex: "\033[38;5;46m", Ascii: "\033[38;5;201m", Debug: "\033[38;5;51m", Info: "\033[38;5;46m", Warn: "\033[38;5;226m", Error: "\033[38;5;196m", Fatal: "\033[1;38;5;196m", Key: "\033[38;5;33m", Number: "\033[38;5;129m", String: "\033[38;5;214m", Bool: "\033[38;5;46m", Time: "\033[38;5;75m", Nil: "\033[38;5;242m", Default: "\033[38;5;15m", JSONKey: "\033[38;5;33m", JSONString: "\033[38;5;214m", JSONNumber: "\033[38;5;129m", JSONBool: "\033[38;5;46m", JSONNull: "\033[38;5;242m", JSONBrace: "\033[38;5;242m", InspectKey: "\033[38;5;33m", InspectValue: "\033[38;5;214m", InspectMeta: "\033[38;5;242m", } // noColorPalette defines a palette with empty strings for environments without color support var noColorPalette = Palette{ Header: "", Goroutine: "", Func: "", Path: "", FileLine: "", Reset: "", Title: "", Pos: "", Hex: "", Ascii: "", Debug: "", Info: "", Warn: "", Error: "", Fatal: "", Key: "", Number: "", String: "", Bool: "", Time: "", Nil: "", Default: "", JSONKey: "", JSONString: "", JSONNumber: "", JSONBool: "", JSONNull: "", JSONBrace: "", InspectKey: "", InspectValue: "", InspectMeta: "", } // colorBufPool is a pool of bytes.Buffer instances to reduce allocations var colorBufPool = sync.Pool{ New: func() interface{} { return &bytes.Buffer{} }, } // ColorizedHandler is a handler that outputs log entries with ANSI color codes. type ColorizedHandler struct { writer io.Writer palette Palette showTime bool timeFormat string mu sync.Mutex noColor bool // Whether to disable colors entirely intensity ColorIntensity // Color intensity level colorFields bool // Whether to colorize fields (default: true) } // ColorOption defines a configuration function for ColorizedHandler. type ColorOption func(*ColorizedHandler) // WithColorPallet sets the color palette for the ColorizedHandler. func WithColorPallet(pallet Palette) ColorOption { return func(c *ColorizedHandler) { c.palette = pallet } } // WithColorNone disables all color output. func WithColorNone() ColorOption { return func(c *ColorizedHandler) { c.noColor = true c.colorFields = false // Also disable field coloring } } // WithColorField enables or disables field coloring specifically. // This is useful for performance optimization or when field colors are too much. // Example: // // handler := NewColorizedHandler(os.Stdout, WithColorField(false)) // Disable field coloring only func WithColorField(enable bool) ColorOption { return func(c *ColorizedHandler) { c.colorFields = enable } } // WithColorShowTime enables or disables the display of timestamps. func WithColorShowTime(show bool) ColorOption { return func(c *ColorizedHandler) { c.showTime = show } } // WithColorIntensity sets the color intensity for the ColorizedHandler. func WithColorIntensity(intensity ColorIntensity) ColorOption { return func(c *ColorizedHandler) { c.intensity = intensity } } // WithColorTheme configures the ColorizedHandler to use a specific color theme based on the provided theme name. func WithColorTheme(theme string) ColorOption { return func(c *ColorizedHandler) { switch strings.ToLower(theme) { case "light": c.palette = lightPalette case "dark": c.palette = darkPalette case "bright": c.palette = brightPalette case "pastel": c.palette = pastelPalette case "vibrant": c.palette = vibrantPalette } } } // NewColorizedHandler creates a new ColorizedHandler writing to the specified writer. func NewColorizedHandler(w io.Writer, opts ...ColorOption) *ColorizedHandler { c := &ColorizedHandler{ writer: w, showTime: false, timeFormat: time.RFC3339, noColor: false, intensity: IntensityNormal, colorFields: true, // Default: enable field coloring } for _, opt := range opts { opt(c) } c.palette = c.detectPalette() return c } func (h *ColorizedHandler) Output(w io.Writer) { h.mu.Lock() defer h.mu.Unlock() h.writer = w } // Handle processes a log entry and writes it with ANSI color codes. func (h *ColorizedHandler) Handle(e *lx.Entry) error { h.mu.Lock() defer h.mu.Unlock() switch e.Class { case lx.ClassDump: return h.handleDumpOutput(e) case lx.ClassJSON, lx.ClassOutput: return h.handleJSONOutput(e) case lx.ClassInspect: return h.handleInspectOutput(e) case lx.ClassRaw: _, err := h.writer.Write([]byte(e.Message)) return err default: return h.handleRegularOutput(e) } } // Timestamped enables or disables timestamp display. 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. func (h *ColorizedHandler) handleRegularOutput(e *lx.Entry) error { buf := colorBufPool.Get().(*bytes.Buffer) buf.Reset() defer colorBufPool.Put(buf) if h.showTime { buf.WriteString(e.Timestamp.Format(h.timeFormat)) buf.WriteString(lx.Space) } h.formatNamespace(buf, e) h.formatLevel(buf, e) buf.WriteString(e.Message) h.formatFields(buf, e) if len(e.Stack) > 0 { h.formatStack(buf, e.Stack) } if e.Level != lx.LevelNone { buf.WriteString(lx.Newline) } _, err := h.writer.Write(buf.Bytes()) return err } // handleJSONOutput handles JSON log entries. func (h *ColorizedHandler) handleJSONOutput(e *lx.Entry) error { buf := colorBufPool.Get().(*bytes.Buffer) buf.Reset() defer colorBufPool.Put(buf) if h.showTime { buf.WriteString(e.Timestamp.Format(h.timeFormat)) buf.WriteString(lx.Newline) } if e.Namespace != "" { h.formatNamespace(buf, e) h.formatLevel(buf, e) } h.colorizeJSON(buf, e.Message) buf.WriteString(lx.Newline) _, err := h.writer.Write(buf.Bytes()) return err } // handleInspectOutput handles inspect log entries. func (h *ColorizedHandler) handleInspectOutput(e *lx.Entry) error { buf := colorBufPool.Get().(*bytes.Buffer) buf.Reset() defer colorBufPool.Put(buf) if h.showTime { buf.WriteString(e.Timestamp.Format(h.timeFormat)) buf.WriteString(lx.Space) } h.formatNamespace(buf, e) h.formatLevel(buf, e) h.colorizeInspect(buf, e.Message) buf.WriteString(lx.Newline) _, err := h.writer.Write(buf.Bytes()) return err } // colorizeJSON applies syntax highlighting to JSON strings without changing formatting func (h *ColorizedHandler) colorizeJSON(b *bytes.Buffer, jsonStr string) { inString := false escapeNext := false for i := 0; i < len(jsonStr); i++ { ch := jsonStr[i] if escapeNext { b.WriteByte(ch) escapeNext = false continue } switch ch { case '\\': escapeNext = true if inString { b.WriteString(h.palette.JSONString) } b.WriteByte(ch) case '"': if inString { // End of string b.WriteString(h.palette.JSONString) b.WriteByte(ch) b.WriteString(h.palette.Reset) inString = false } else { // Start of string inString = true b.WriteString(h.palette.JSONString) b.WriteByte(ch) } case ':': if !inString { b.WriteString(h.palette.JSONBrace) b.WriteByte(ch) b.WriteString(h.palette.Reset) } else { b.WriteByte(ch) } case '{', '}', '[', ']', ',': if !inString { b.WriteString(h.palette.JSONBrace) b.WriteByte(ch) b.WriteString(h.palette.Reset) } else { b.WriteByte(ch) } default: if !inString { // Check for numbers, booleans, null remaining := jsonStr[i:] // Check for null if len(remaining) >= 4 && strings.HasPrefix(remaining, "null") { b.WriteString(h.palette.JSONNull) b.WriteString("null") b.WriteString(h.palette.Reset) i += 3 // Skip "null" } else if len(remaining) >= 4 && strings.HasPrefix(remaining, "true") { b.WriteString(h.palette.JSONBool) b.WriteString("true") b.WriteString(h.palette.Reset) i += 3 // Skip "true" } else if len(remaining) >= 5 && strings.HasPrefix(remaining, "false") { b.WriteString(h.palette.JSONBool) b.WriteString("false") b.WriteString(h.palette.Reset) i += 4 // Skip "false" } else if (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' { b.WriteString(h.palette.JSONNumber) b.WriteByte(ch) // Continue writing digits for j := i + 1; j < len(jsonStr); j++ { nextCh := jsonStr[j] if (nextCh >= '0' && nextCh <= '9') || nextCh == '.' || nextCh == 'e' || nextCh == 'E' || nextCh == '+' || nextCh == '-' { b.WriteByte(nextCh) i = j } else { break } } b.WriteString(h.palette.Reset) } else if ch == ' ' || ch == '\n' || ch == '\t' || ch == '\r' { // Preserve whitespace exactly as is b.WriteByte(ch) } else { // Unexpected character outside string - preserve it b.WriteByte(ch) } } else { // Inside string b.WriteByte(ch) } } } } // colorizeInspect applies syntax highlighting to inspect output func (h *ColorizedHandler) colorizeInspect(b *bytes.Buffer, inspectStr string) { lines := strings.Split(inspectStr, "\n") for lineIdx, line := range lines { if lineIdx > 0 { b.WriteString("\n") } trimmed := strings.TrimSpace(line) if trimmed == "" { b.WriteString(line) continue } // For inspect output, we'll do simple line-based coloring // This preserves the original formatting inString := false escapeNext := false for i := 0; i < len(line); i++ { ch := line[i] if escapeNext { b.WriteByte(ch) escapeNext = false continue } if ch == '\\' { escapeNext = true b.WriteByte(ch) continue } if ch == '"' { inString = !inString if inString { // Check if this is a metadata key if i+1 < len(line) && line[i+1] == '(' { b.WriteString(h.palette.InspectMeta) } else if i+2 < len(line) && line[i+1] == '*' && line[i+2] == '(' { b.WriteString(h.palette.InspectMeta) } else { b.WriteString(h.palette.InspectKey) } } b.WriteByte(ch) if !inString { b.WriteString(h.palette.Reset) } continue } if inString { // Inside a string key or value b.WriteByte(ch) } else { // Outside strings if ch == ':' { b.WriteString(h.palette.JSONBrace) b.WriteByte(ch) b.WriteString(h.palette.Reset) } else if ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == ',' { b.WriteString(h.palette.JSONBrace) b.WriteByte(ch) b.WriteString(h.palette.Reset) } else { // Check for numbers, booleans, null outside strings remaining := line[i:] if len(remaining) >= 4 && strings.HasPrefix(remaining, "null") { b.WriteString(h.palette.JSONNull) b.WriteString("null") b.WriteString(h.palette.Reset) i += 3 } else if len(remaining) >= 4 && strings.HasPrefix(remaining, "true") { b.WriteString(h.palette.JSONBool) b.WriteString("true") b.WriteString(h.palette.Reset) i += 3 } else if len(remaining) >= 5 && strings.HasPrefix(remaining, "false") { b.WriteString(h.palette.JSONBool) b.WriteString("false") b.WriteString(h.palette.Reset) i += 4 } else if (ch >= '0' && ch <= '9') || ch == '-' { b.WriteString(h.palette.InspectValue) b.WriteByte(ch) // Continue writing digits for j := i + 1; j < len(line); j++ { nextCh := line[j] if (nextCh >= '0' && nextCh <= '9') || nextCh == '.' { b.WriteByte(nextCh) i = j } else { break } } b.WriteString(h.palette.Reset) } else { b.WriteByte(ch) } } } } } } // formatNamespace formats the namespace with ANSI color codes. func (h *ColorizedHandler) formatNamespace(b *bytes.Buffer, e *lx.Entry) { if e.Namespace == "" { return } b.WriteString(lx.LeftBracket) switch e.Style { case lx.NestedPath: 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: 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. func (h *ColorizedHandler) formatLevel(b *bytes.Buffer, e *lx.Entry) { color := map[lx.LevelType]string{ lx.LevelDebug: h.palette.Debug, lx.LevelInfo: h.palette.Info, lx.LevelWarn: h.palette.Warn, lx.LevelError: h.palette.Error, lx.LevelFatal: h.palette.Fatal, }[e.Level] b.WriteString(color) b.WriteString(e.Level.Name(e.Class)) b.WriteString(h.palette.Reset) // b.WriteString(lx.Space) b.WriteString(lx.Colon) b.WriteString(lx.Space) } // formatFields formats the log entry's fields in sorted order. func (h *ColorizedHandler) formatFields(b *bytes.Buffer, e *lx.Entry) { if len(e.Fields) == 0 { return } b.WriteString(lx.Space) b.WriteString(lx.LeftBracket) for i, pair := range e.Fields { if i > 0 { b.WriteString(lx.Space) } if h.colorFields { // Color the key b.WriteString(h.palette.Key) b.WriteString(pair.Key) b.WriteString(h.palette.Reset) b.WriteString("=") // Format value with type-based coloring h.formatFieldValue(b, pair.Value) } else { // No field coloring - just write plain text b.WriteString(pair.Key) b.WriteString("=") writeFieldValue(b, pair.Value) } } b.WriteString(lx.RightBracket) } // formatFieldValue formats a field value with type-based ANSI color codes. func (h *ColorizedHandler) formatFieldValue(b *bytes.Buffer, value interface{}) { // If field coloring is disabled, just write the value if !h.colorFields { writeFieldValue(b, value) return } switch v := value.(type) { case time.Time: b.WriteString(h.palette.Time) b.WriteString(v.Format("2006-01-02 15:04:05")) b.WriteString(h.palette.Reset) case time.Duration: b.WriteString(h.palette.Time) h.formatDuration(b, v) b.WriteString(h.palette.Reset) case error: b.WriteString(h.palette.Error) b.WriteString(`"`) b.WriteString(v.Error()) b.WriteString(`"`) b.WriteString(h.palette.Reset) case int, int8, int16, int32, int64: b.WriteString(h.palette.Number) writeFieldValue(b, v) b.WriteString(h.palette.Reset) case uint, uint8, uint16, uint32, uint64: b.WriteString(h.palette.Number) writeFieldValue(b, v) b.WriteString(h.palette.Reset) case float32, float64: b.WriteString(h.palette.Number) writeFieldValue(b, v) b.WriteString(h.palette.Reset) case string: b.WriteString(h.palette.String) b.WriteString(`"`) b.WriteString(v) b.WriteString(`"`) b.WriteString(h.palette.Reset) case bool: b.WriteString(h.palette.Bool) writeFieldValue(b, v) b.WriteString(h.palette.Reset) case nil: b.WriteString(h.palette.Nil) b.WriteString("nil") b.WriteString(h.palette.Reset) default: b.WriteString(h.palette.Default) writeFieldValue(b, v) b.WriteString(h.palette.Reset) } } // formatDuration formats a duration in a human-readable way func (h *ColorizedHandler) formatDuration(b *bytes.Buffer, d time.Duration) { if d < time.Microsecond { b.WriteString(d.String()) } else if d < time.Millisecond { fmt.Fprintf(b, "%.3fµs", float64(d)/float64(time.Microsecond)) } else if d < time.Second { fmt.Fprintf(b, "%.3fms", float64(d)/float64(time.Millisecond)) } else if d < time.Minute { fmt.Fprintf(b, "%.3fs", float64(d)/float64(time.Second)) } else if d < time.Hour { minutes := d / time.Minute seconds := (d % time.Minute) / time.Second fmt.Fprintf(b, "%dm%.3fs", minutes, float64(seconds)/float64(time.Second)) } else { hours := d / time.Hour minutes := (d % time.Hour) / time.Minute fmt.Fprintf(b, "%dh%dm", hours, minutes) } } // formatStack formats a stack trace with ANSI color codes. func (h *ColorizedHandler) formatStack(b *bytes.Buffer, 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 } b.WriteString(" ┌─ ") b.WriteString(h.palette.Goroutine) b.WriteString(lines[0]) b.WriteString(h.palette.Reset) b.WriteString("\n") 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(" │ ") lastSlash := strings.LastIndex(pathLine, "/") goIndex := strings.Index(pathLine, ".go:") if lastSlash >= 0 && goIndex > lastSlash { prefix := pathLine[:lastSlash+1] suffix := pathLine[lastSlash+1:] b.WriteString(h.palette.Path) b.WriteString(prefix) b.WriteString(h.palette.Reset) b.WriteString(h.palette.Path) b.WriteString(suffix) b.WriteString(h.palette.Reset) } else { b.WriteString(h.palette.Path) b.WriteString(pathLine) b.WriteString(h.palette.Reset) } b.WriteString("\n") } } 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. func (h *ColorizedHandler) handleDumpOutput(e *lx.Entry) error { buf := colorBufPool.Get().(*bytes.Buffer) buf.Reset() defer colorBufPool.Put(buf) if h.showTime { buf.WriteString(e.Timestamp.Format(h.timeFormat)) buf.WriteString(lx.Newline) } buf.WriteString(h.palette.Title) buf.WriteString("---- BEGIN DUMP ----") buf.WriteString(h.palette.Reset) buf.WriteString("\n") lines := strings.Split(e.Message, "\n") length := len(lines) for i, line := range lines { if strings.HasPrefix(line, "pos ") { parts := strings.SplitN(line, "hex:", 2) if len(parts) == 2 { buf.WriteString(h.palette.Pos) buf.WriteString(parts[0]) buf.WriteString(h.palette.Reset) hexAscii := strings.SplitN(parts[1], "'", 2) buf.WriteString(h.palette.Hex) buf.WriteString("hex:") buf.WriteString(hexAscii[0]) buf.WriteString(h.palette.Reset) if len(hexAscii) > 1 { buf.WriteString(h.palette.Ascii) buf.WriteString("'") buf.WriteString(hexAscii[1]) buf.WriteString(h.palette.Reset) } } } else if strings.HasPrefix(line, "Dumping value of type:") { buf.WriteString(h.palette.Header) buf.WriteString(line) buf.WriteString(h.palette.Reset) } else { buf.WriteString(line) } if i < length-1 { buf.WriteString("\n") } } buf.WriteString(h.palette.Title) buf.WriteString("---- END DUMP ----") buf.WriteString(h.palette.Reset) buf.WriteString("\n") _, err := h.writer.Write(buf.Bytes()) return err } // paletteCache stores the detected palette and environment hash to avoid repeated checks. var paletteCache struct { once sync.Once mu sync.RWMutex hash uint64 palette Palette } // computeEnvHash generates a hash of relevant environment variables to detect changes. func computeEnvHash() uint64 { h := fnv.New64a() h.Write([]byte(os.Getenv("NO_COLOR"))) h.Write([]byte(os.Getenv("TERM"))) h.Write([]byte(os.Getenv("COLORFGBG"))) h.Write([]byte(os.Getenv("AppleInterfaceStyle"))) h.Write([]byte(os.Getenv("APPEARANCE"))) h.Write([]byte(os.Getenv("TERM_BACKGROUND"))) return h.Sum64() } // computePaletteFromEnv computes the palette based on current environment variables. // This contains the original logic from detectPalette, extracted for caching. func (h *ColorizedHandler) computePaletteFromEnv() Palette { if os.Getenv("NO_COLOR") != "" { return noColorPalette } term := os.Getenv("TERM") if term == "dumb" { return noColorPalette } // Windows is handled via build tag, just check terminal capability if h.isWindowsTerminal() { return h.applyIntensity(darkPalette) } // Unix/macOS background detection isDarkBackground := true if style, ok := os.LookupEnv("APPEARANCE"); ok && strings.EqualFold(style, "light") { isDarkBackground = false } else if fgBg := os.Getenv("COLORFGBG"); fgBg != "" { parts := strings.Split(fgBg, ";") if len(parts) >= 2 { bgInt, _ := strconv.Atoi(parts[len(parts)-1]) isDarkBackground = (bgInt >= 0 && bgInt <= 6) || (bgInt >= 8 && bgInt <= 14) } } if isDarkBackground { return h.applyIntensity(darkPalette) } return h.applyIntensity(lightPalette) } // detectPalette selects a color palette based on terminal environment variables. // It uses caching with environment hash to avoid repeated checks. func (h *ColorizedHandler) detectPalette() Palette { // If colors are explicitly disabled, return noColorPalette if h.noColor { return noColorPalette } // Fast path: Check cache currentHash := computeEnvHash() paletteCache.mu.RLock() if paletteCache.hash == currentHash && paletteCache.palette != (Palette{}) { p := paletteCache.palette paletteCache.mu.RUnlock() return p } paletteCache.mu.RUnlock() // Slow path: Compute paletteCache.mu.Lock() defer paletteCache.mu.Unlock() // Double-check after acquiring write lock if paletteCache.hash == currentHash && paletteCache.palette != (Palette{}) { return paletteCache.palette } // Compute new palette p := h.computePaletteFromEnv() paletteCache.hash = currentHash paletteCache.palette = p return p } // applyIntensity applies the intensity setting to a base palette func (h *ColorizedHandler) applyIntensity(basePalette Palette) Palette { switch h.intensity { case IntensityNormal: return basePalette case IntensityBright: return brightPalette case IntensityPastel: return pastelPalette case IntensityVibrant: return vibrantPalette default: return basePalette } } golang-github-olekukonko-ll-0.1.8/lh/colorized_unix.go000066400000000000000000000002601516152337300230260ustar00rootroot00000000000000//go:build !windows package lh // No-op for Unix systems - ANSI is native. func enableWindowsANSI() {} func (h *ColorizedHandler) isWindowsTerminal() bool { return false } golang-github-olekukonko-ll-0.1.8/lh/colorized_windows.go000066400000000000000000000021071516152337300235370ustar00rootroot00000000000000//go:build windows package lh import ( "os" "syscall" "unsafe" ) func init() { enableWindowsANSI() } // enableWindowsANSI enables virtual terminal processing on Windows 10+. func enableWindowsANSI() { kernel32 := syscall.NewLazyDLL("kernel32.dll") setConsoleMode := kernel32.NewProc("SetConsoleMode") getConsoleMode := kernel32.NewProc("GetConsoleMode") const enableVirtualTerminalProcessing = 0x0004 handles := []syscall.Handle{syscall.Stdout, syscall.Stderr} for _, handle := range handles { var mode uint32 if r, _, _ := getConsoleMode.Call(uintptr(handle), uintptr(unsafe.Pointer(&mode))); r != 0 { if mode&enableVirtualTerminalProcessing == 0 { newMode := mode | enableVirtualTerminalProcessing setConsoleMode.Call(uintptr(handle), uintptr(newMode)) } } } } // isWindowsTerminal checks Windows-specific terminal indicators. func (h *ColorizedHandler) isWindowsTerminal() bool { if os.Getenv("WT_SESSION") != "" { return true } if os.Getenv("ConEmuANSI") == "ON" { return true } if os.Getenv("ANSICON") != "" { return true } return false } golang-github-olekukonko-ll-0.1.8/lh/dedup.go000066400000000000000000000137651516152337300211100ustar00rootroot00000000000000package lh import ( "bytes" "fmt" "sort" "sync" "time" "github.com/cespare/xxhash/v2" "github.com/olekukonko/ll/lx" ) // shardCount determines the number of shards for the dedup handler. // Must be a power of 2 for efficient modulo via bitwise AND. const shardCount = 32 // Dedup is a log handler that suppresses duplicate entries within a TTL window. // It wraps another handler and filters out repeated log entries that match // within the deduplication period. type Dedup struct { next lx.Handler ttl time.Duration cleanupEvery time.Duration keyFn lx.Deduper maxKeys int shards [shardCount]dedupShard // value array; take &shards[i] when locking done chan struct{} wg sync.WaitGroup once sync.Once } type dedupShard struct { mu sync.Mutex seen map[uint64]int64 // key -> expiry unix-nano timestamp } // DedupOpt configures a Dedup handler. type DedupOpt func(*Dedup) // WithDedupKeyFunc customizes how deduplication keys are generated. func WithDedupKeyFunc(fn func(*lx.Entry) uint64) DedupOpt { return func(d *Dedup) { d.keyFn = dedupKeyFunc(fn) } } type dedupKeyFunc func(*lx.Entry) uint64 func (f dedupKeyFunc) Calculate(e *lx.Entry) uint64 { return f(e) } // WithDedupCleanupInterval sets how often expired deduplication keys are purged. func WithDedupCleanupInterval(every time.Duration) DedupOpt { return func(d *Dedup) { if every > 0 { d.cleanupEvery = every } } } // WithDedupMaxKeys sets a soft limit on tracked deduplication keys. func WithDedupMaxKeys(max int) DedupOpt { return func(d *Dedup) { if max > 0 { d.maxKeys = max } } } // WithDedupIgnore specifies fields to ignore in the default key function. func WithDedupIgnore(fields ...string) DedupOpt { return func(d *Dedup) { if dd, ok := d.keyFn.(*defaultDedup); ok { if dd.ignoreFields == nil { dd.ignoreFields = make(map[string]struct{}, len(fields)) } for _, f := range fields { dd.ignoreFields[f] = struct{}{} } } } } // NewDedup creates a deduplicating handler wrapper. func NewDedup(next lx.Handler, ttl time.Duration, opts ...DedupOpt) *Dedup { if ttl <= 0 { ttl = 2 * time.Second } d := &Dedup{ next: next, ttl: ttl, cleanupEvery: time.Minute, keyFn: NewDefaultDedup(), done: make(chan struct{}), } // Pre-allocate each shard's map to avoid growth allocations at startup. for i := 0; i < len(d.shards); i++ { d.shards[i].seen = make(map[uint64]int64, 64) } for _, opt := range opts { opt(d) } d.wg.Add(1) go d.cleanupLoop() return d } // Handle processes a log entry, suppressing duplicates within the TTL window. func (d *Dedup) Handle(e *lx.Entry) error { // Guard against nil keyFn — pass through if not configured. if d.keyFn == nil { return d.next.Handle(e) } now := time.Now().UnixNano() key := d.keyFn.Calculate(e) // Bitwise AND is safe because shardCount is a power of 2. shard := &d.shards[key&(shardCount-1)] shard.mu.Lock() exp, ok := shard.seen[key] if ok && now < exp { shard.mu.Unlock() return nil // duplicate within TTL — suppress } // Opportunistic per-shard cleanup when the shard is getting full. if d.maxKeys > 0 { limitPerShard := d.maxKeys / shardCount if limitPerShard > 0 && len(shard.seen) >= limitPerShard { d.cleanupShardLocked(shard, now) } } shard.seen[key] = now + d.ttl.Nanoseconds() shard.mu.Unlock() return d.next.Handle(e) } // getShardIndex returns the shard index for a given key. // Uses bitwise AND since shardCount is a power of 2. func (d *Dedup) getShardIndex(key uint64) int { return int(key & (shardCount - 1)) } // Close stops the cleanup goroutine and closes the underlying handler. func (d *Dedup) Close() error { var err error d.once.Do(func() { close(d.done) d.wg.Wait() if c, ok := d.next.(interface{ Close() error }); ok { err = c.Close() } }) return err } // cleanupLoop runs periodically to purge expired deduplication keys. func (d *Dedup) cleanupLoop() { defer d.wg.Done() ticker := time.NewTicker(d.cleanupEvery) defer ticker.Stop() for { select { case <-ticker.C: now := time.Now().UnixNano() for i := 0; i < len(d.shards); i++ { shard := &d.shards[i] shard.mu.Lock() d.cleanupShardLocked(shard, now) shard.mu.Unlock() } case <-d.done: return } } } // cleanupShardLocked removes expired keys from a shard (caller must hold lock). func (d *Dedup) cleanupShardLocked(shard *dedupShard, now int64) { for k, exp := range shard.seen { if now > exp { delete(shard.seen, k) } } } // defaultDedup implements the default deduplication key calculation. type defaultDedup struct { ignoreFields map[string]struct{} } // NewDefaultDedup creates a new default deduplication key generator. func NewDefaultDedup() lx.Deduper { return &defaultDedup{ignoreFields: make(map[string]struct{})} } // Calculate generates a deduplication key from level, message, namespace, and // fields. Fields are sorted before hashing so that identical entries always // produce the same key regardless of Go map iteration order. func (d *defaultDedup) Calculate(e *lx.Entry) uint64 { h := xxhash.New() zero := []byte{0} h.Write([]byte(e.Level.String())) h.Write(zero) h.Write([]byte(e.Message)) h.Write(zero) h.Write([]byte(e.Namespace)) h.Write(zero) if len(e.Fields) > 0 { m := e.Fields.Map() keys := make([]string, 0, len(m)) for k := range m { if _, excluded := d.ignoreFields[k]; !excluded { keys = append(keys, k) } } // Sort keys to guarantee a deterministic hash across calls. // Without this, Go's random map iteration order means two identical // entries can hash to different values and bypass deduplication. sort.Strings(keys) buf := dedupBufPool.Get().(*bytes.Buffer) buf.Reset() defer dedupBufPool.Put(buf) for _, k := range keys { buf.WriteString(k) buf.WriteByte('=') fmt.Fprint(buf, m[k]) buf.WriteByte(0) } h.Write(buf.Bytes()) } return h.Sum64() } var dedupBufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } golang-github-olekukonko-ll-0.1.8/lh/dedup_test.go000066400000000000000000000057401516152337300221410ustar00rootroot00000000000000package lh import ( "sync/atomic" "testing" "time" "github.com/olekukonko/ll/lx" ) // TestDedup_ShardDistribution tests that entries are distributed across shards func TestDedup_ShardDistribution(t *testing.T) { handler := &countingHandler{} d := NewDedup(handler, time.Second) // Send entries with different keys for i := 0; i < 1000; i++ { entry := &lx.Entry{ Level: lx.LevelInfo, Message: string(rune('a' + (i % 26))), } d.Handle(entry) } // Check that shards were used usedShards := 0 for i := 0; i < len(d.shards); i++ { d.shards[i].mu.Lock() if len(d.shards[i].seen) > 0 { usedShards++ } d.shards[i].mu.Unlock() } if usedShards < 2 { t.Fatalf("expected distribution across multiple shards, got %d", usedShards) } d.Close() } // TestDedup_NilKeyFn tests panic protection func TestDedup_NilKeyFn(t *testing.T) { handler := &countingHandler{} d := NewDedup(handler, time.Second) d.keyFn = nil // Simulate bug // Should not panic, should pass through err := d.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "test"}) if err != nil { t.Fatalf("expected nil error with nil keyFn, got %v", err) } d.Close() } // TestDedup_TTLExpiration tests that entries expire correctly func TestDedup_TTLExpiration(t *testing.T) { handler := &countingHandler{} ttl := 50 * time.Millisecond d := NewDedup(handler, ttl, WithDedupCleanupInterval(10*time.Millisecond)) // First entry d.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "duplicate"}) // Same entry immediately - should be deduped handler.count.Store(0) d.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "duplicate"}) if handler.count.Load() != 0 { t.Fatal("entry should have been deduped") } // Wait for TTL time.Sleep(ttl + 20*time.Millisecond) // Same entry after TTL - should be allowed handler.count.Store(0) d.Handle(&lx.Entry{Level: lx.LevelInfo, Message: "duplicate"}) if handler.count.Load() != 1 { t.Fatal("entry should have been allowed after TTL") } d.Close() } type countingHandler struct { count atomic.Int32 } func (c *countingHandler) Handle(e *lx.Entry) error { c.count.Add(1) return nil } // BenchmarkDedup_ShardContention tests concurrent performance func BenchmarkDedup_ShardContention(b *testing.B) { handler := &countingHandler{} d := NewDedup(handler, time.Second) b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { entry := &lx.Entry{ Level: lx.LevelInfo, Message: string(rune('a' + (i % 26))), } d.Handle(entry) i++ } }) d.Close() } func TestDedup_SuppressesWithFields(t *testing.T) { handler := &countingHandler{} d := NewDedup(handler, time.Second) entry := &lx.Entry{ Level: lx.LevelInfo, Message: "login failed", Fields: lx.Fields{ {Key: "user", Value: "alice"}, {Key: "ip", Value: "1.2.3.4"}, }, } d.Handle(entry) handler.count.Store(0) // Identical entry — must be suppressed d.Handle(entry) if handler.count.Load() != 0 { t.Fatal("entry with fields should have been deduped") } d.Close() } golang-github-olekukonko-ll-0.1.8/lh/json.go000066400000000000000000000203431516152337300207460ustar00rootroot00000000000000package lh import ( "bytes" "fmt" "io" "os" "strings" "sync" "time" "github.com/goccy/go-json" "github.com/olekukonko/ll/lx" ) var jsonBufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } // fieldsMapPool pools map[string]interface{} to reduce allocations var fieldsMapPool = sync.Pool{ New: func() any { return make(map[string]interface{}, 8) }, } // jsonOutputPool pools JsonOutput structs to reduce allocations var jsonOutputPool = sync.Pool{ New: func() any { return &JsonOutput{ Fields: make(map[string]interface{}, 8), } }, } // 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 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) } // Output sets the Writer destination for JSONHandler's output, ensuring thread safety with a mutex lock. func (h *JSONHandler) Output(w io.Writer) { h.mu.Lock() defer h.mu.Unlock() h.writer = w } // 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 { // Get fieldsMap from pool to avoid allocation fieldsMap := fieldsMapPool.Get().(map[string]interface{}) // Clear any existing keys from previous use for k := range fieldsMap { delete(fieldsMap, k) } // Convert ordered fields to map for JSON output for _, pair := range e.Fields { fieldsMap[pair.Key] = pair.Value } // Get JsonOutput from pool entry := jsonOutputPool.Get().(*JsonOutput) entry.Time = e.Timestamp.Format(h.timeFmt) entry.Level = e.Level.String() entry.Class = e.Class.String() entry.Msg = e.Message entry.Namespace = e.Namespace entry.Dump = nil entry.Fields = fieldsMap entry.Stack = e.Stack // Acquire buffer from pool to avoid allocation and reduce syscalls buf := jsonBufPool.Get().(*bytes.Buffer) buf.Reset() defer func() { // Return all pooled objects jsonBufPool.Put(buf) // Reset and return fieldsMap to pool for k := range entry.Fields { delete(entry.Fields, k) } fieldsMapPool.Put(entry.Fields) // Reset and return JsonOutput to pool entry.Fields = nil entry.Stack = nil entry.Dump = nil jsonOutputPool.Put(entry) }() // Create JSON encoder writing to buffer (uses go-json for 2-5x speedup) enc := json.NewEncoder(buf) if h.pretty { // Enable indentation for pretty printing enc.SetIndent("", " ") } // Encode JSON to buffer err := enc.Encode(entry) if err != nil { // Log encoding error for debugging fmt.Fprintf(os.Stderr, "JSON encode error: %v\n", err) return err } // Write buffer to underlying writer in one go _, err = h.writer.Write(buf.Bytes()) 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 }) } // Get fieldsMap from pool fieldsMap := fieldsMapPool.Get().(map[string]interface{}) for k := range fieldsMap { delete(fieldsMap, k) } for _, pair := range e.Fields { fieldsMap[pair.Key] = pair.Value } // Get JsonOutput from pool entry := jsonOutputPool.Get().(*JsonOutput) entry.Time = e.Timestamp.Format(h.timeFmt) entry.Level = e.Level.String() entry.Class = e.Class.String() entry.Msg = "dumping segments" entry.Namespace = e.Namespace entry.Dump = segments entry.Fields = fieldsMap entry.Stack = e.Stack // Acquire buffer from pool buf := jsonBufPool.Get().(*bytes.Buffer) buf.Reset() defer func() { jsonBufPool.Put(buf) for k := range entry.Fields { delete(entry.Fields, k) } fieldsMapPool.Put(entry.Fields) entry.Fields = nil entry.Stack = nil entry.Dump = nil jsonOutputPool.Put(entry) }() // Encode JSON output with dump segments to buffer enc := json.NewEncoder(buf) if h.pretty { enc.SetIndent("", " ") } err := enc.Encode(entry) if err != nil { fmt.Fprintf(os.Stderr, "JSON dump encode error: %v\n", err) return err } // Write buffer to underlying writer _, err = h.writer.Write(buf.Bytes()) return err } golang-github-olekukonko-ll-0.1.8/lh/lh.go000066400000000000000000000037371516152337300204100ustar00rootroot00000000000000package lh import ( "fmt" "strconv" "strings" ) // rightPad pads a string with spaces on the right to reach the specified length. // Returns the original string if it's already at or exceeds the target length. // Uses strings.Builder for efficient memory allocation. func rightPad(str string, length int) string { if len(str) >= length { return str } var sb strings.Builder sb.Grow(length) sb.WriteString(str) sb.WriteString(strings.Repeat(" ", length-len(str))) return sb.String() } // stringWriter is the interface for types that can write strings and bytes. // Both *strings.Builder and *bytes.Buffer implement this. type stringWriter interface { WriteString(s string) (int, error) Write(p []byte) (n int, err error) } // writeFieldValue writes a field value to the builder using type switches // to avoid reflection and allocations associated with fmt.Fprint. func writeFieldValue(b stringWriter, v interface{}) { switch val := v.(type) { case string: b.WriteString(val) case int: b.WriteString(strconv.Itoa(val)) case int8: b.WriteString(strconv.FormatInt(int64(val), 10)) case int16: b.WriteString(strconv.FormatInt(int64(val), 10)) case int32: b.WriteString(strconv.FormatInt(int64(val), 10)) case int64: b.WriteString(strconv.FormatInt(val, 10)) case uint: b.WriteString(strconv.FormatUint(uint64(val), 10)) case uint8: b.WriteString(strconv.FormatUint(uint64(val), 10)) case uint16: b.WriteString(strconv.FormatUint(uint64(val), 10)) case uint32: b.WriteString(strconv.FormatUint(uint64(val), 10)) case uint64: b.WriteString(strconv.FormatUint(val, 10)) case float32: b.WriteString(strconv.FormatFloat(float64(val), 'g', -1, 32)) case float64: b.WriteString(strconv.FormatFloat(val, 'g', -1, 64)) case bool: if val { b.WriteString("true") } else { b.WriteString("false") } case nil: b.WriteString("nil") case error: b.WriteString(val.Error()) case fmt.Stringer: b.WriteString(val.String()) default: fmt.Fprint(b, val) } } golang-github-olekukonko-ll-0.1.8/lh/memory.go000066400000000000000000000074661516152337300213200ustar00rootroot00000000000000package lh import ( "fmt" "io" "sync" "github.com/olekukonko/ll/lx" ) // 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: %writer", err) // Wrap and return write errors } } return nil } golang-github-olekukonko-ll-0.1.8/lh/multi.go000066400000000000000000000053261516152337300211330ustar00rootroot00000000000000package 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 } } // Len returns the number of handlers in the MultiHandler. // Useful for monitoring or debugging handler composition. // // Example: // // multi := &MultiHandler{} // multi.Append(h1, h2, h3) // count := multi.Len() // Returns 3 func (h *MultiHandler) Len() int { return len(h.Handlers) } // Append adds one or more handlers to the MultiHandler. // Handlers will receive log entries in the order they were appended. // This method modifies the MultiHandler in place. // // Example: // // multi := &MultiHandler{} // multi.Append( // lx.NewJSONHandler(os.Stdout), // lx.NewTextHandler(logFile), // ) // // Now multi broadcasts to both stdout and file func (h *MultiHandler) Append(handlers ...lx.Handler) { h.Handlers = append(h.Handlers, 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: %writer", i, err)) } } // Combine errors into a single error, or return nil if no errors return errors.Join(errs...) } golang-github-olekukonko-ll-0.1.8/lh/pipe.go000066400000000000000000000046041516152337300207340ustar00rootroot00000000000000package lh import ( "fmt" "os" "time" "github.com/olekukonko/ll/lx" ) // Pipe chains multiple handler wrappers together, applying them from left to right. // The wrappers are composed such that the first wrapper in the list becomes // the innermost layer, and the last wrapper becomes the outermost layer. // // Usage pattern: Pipe(baseHandler, wrapper1, wrapper2, wrapper3) // Result: wrapper3(wrapper2(wrapper1(baseHandler))) // // This enables clean, declarative construction of handler middleware chains. // // Example - building a processing pipeline: // // base := lx.NewJSONHandler(os.Stdout) // handler := lh.Pipe(base, // lh.NewDedup(2*time.Second), // 1. Deduplicate first // lh.NewRateLimit(10, time.Second), // 2. Then rate limit // ) // logger := lx.NewLogger(handler) // // In this example, logs flow: Dedup → RateLimit → AddTimestamp → JSONHandler func Pipe(h lx.Handler, wraps ...lx.Wrap) lx.Handler { for _, w := range wraps { if w != nil { h = w(h) } } return h } // PipeDedup returns a wrapper that applies deduplication to the handler. func PipeDedup(ttl time.Duration, opts ...DedupOpt) lx.Wrap { return func(next lx.Handler) lx.Handler { return NewDedup(next, ttl, opts...) } } // PipeBuffer returns a wrapper that applies buffering to the handler. func PipeBuffer(opts ...BufferingOpt) lx.Wrap { return func(next lx.Handler) lx.Handler { return NewBuffered(next, opts...) } } // PipeRotate returns a wrapper that applies log rotation. // Ideally, the 'next' handler should be one that writes to a file (like TextHandler or JSONHandler). // // If the underlying handler does not implement lx.HandlerOutputter (cannot change output destination), // or if rotation initialization fails, this will log a warning to stderr and return the // original handler unmodified to prevent application crashes. func PipeRotate(maxSizeBytes int64, src RotateSource) lx.Wrap { return func(next lx.Handler) lx.Handler { // Attempt to cast to HandlerOutputter (Handler + Outputter interface) h, ok := next.(lx.HandlerOutputter) if !ok { fmt.Fprintf(os.Stderr, "ll/lh: PipeRotate skipped - handler does not implement SetOutput(io.Writer)\n") return next } // Initialize the rotating handler r, err := NewRotating(h, maxSizeBytes, src) if err != nil { fmt.Fprintf(os.Stderr, "ll/lh: PipeRotate initialization failed: %v\n", err) return next } return r } } golang-github-olekukonko-ll-0.1.8/lh/rotate.go000066400000000000000000000153301516152337300212730ustar00rootroot00000000000000package lh import ( "io" "sync" "sync/atomic" "github.com/olekukonko/ll/lx" ) // trackingWriter wraps an io.WriteCloser to keep an in-memory count of bytes written. // This prevents the rotator from having to query the filesystem (via os.Stat) // on every single log entry, which would cause severe performance bottlenecks. type trackingWriter struct { io.WriteCloser written int64 // Atomic: use atomic.LoadInt64/AddInt64 } // Write intercepts the write operation, counts the bytes, and passes them to the underlying writer. func (t *trackingWriter) Write(p []byte) (n int, err error) { n, err = t.WriteCloser.Write(p) if n > 0 { atomic.AddInt64(&t.written, int64(n)) } return } // writtenBytes returns the current byte count atomically. func (t *trackingWriter) writtenBytes() int64 { if t == nil { return 0 } return atomic.LoadInt64(&t.written) } // RotateSource defines the callbacks needed to implement log rotation. // It abstracts the destination lifecycle: opening, sizing, and rotating. // // Example for file rotation: // // src := lh.RotateSource{ // Open: func() (io.WriteCloser, error) { // return os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) // }, // Size: func() (int64, error) { // if fi, err := os.Stat("app.log"); err == nil { // return fi.Size(), nil // } // return 0, nil // File doesn't exist yet // }, // Rotate: func() error { // // Close and rename the current log before creating a new one. // return os.Rename("app.log", "app.log."+time.Now().Format("20060102-150405")) // }, // } type RotateSource struct { // Open returns a fresh destination for log output. // Called on initialization and after each rotation. Open func() (io.WriteCloser, error) // Size returns the current size in bytes of the active destination. // Return an error if size cannot be determined (rotation will be skipped). Size func() (int64, error) // Rotate performs all cleanup/rotation actions before a new destination is // opened, including closing or renaming the previous writer when required. // Rotating will NOT close the old writer itself; that is the responsibility // of this callback. May be nil if no pre-open actions are needed. Rotate func() error } // Rotating wraps a handler to rotate its output when maxSize is exceeded. // The wrapped handler must implement both Handler and Outputter interfaces. // Rotation is triggered on each Handle call if the current size >= maxSize. // // Example: // // handler := lx.NewJSONHandler(os.Stdout) // src := lh.RotateSource{...} // see RotateSource example // rotator, err := lh.NewRotating(handler, 10*1024*1024, src) // 10 MB // logger := lx.NewLogger(rotator) // logger.Info("This log may trigger rotation when file reaches 10MB") type Rotating[H interface { lx.Handler lx.Outputter }] struct { mu sync.Mutex maxSize int64 src RotateSource out *trackingWriter // Uses the tracking wrapper to count bytes in memory handler H } // NewRotating creates a rotating wrapper around handler. // Handler's output will be replaced with destinations from src.Open. // If maxSizeBytes <= 0, rotation is disabled. // src.Rotate may be nil if no pre-open actions are needed. // // Example: // // // Create a JSON handler that rotates at 5MB // handler := lx.NewJSONHandler(os.Stdout) // rotator, err := lh.NewRotating(handler, 5*1024*1024, src) // if err != nil { // log.Fatal(err) // } // // Use rotator as your logger's handler // logger := lx.NewLogger(rotator) func NewRotating[H interface { lx.Handler lx.Outputter }](handler H, maxSizeBytes int64, src RotateSource) (*Rotating[H], error) { // Validate that Open callback is provided if src.Open == nil { return nil, io.ErrClosedPipe } r := &Rotating[H]{ maxSize: maxSizeBytes, src: src, handler: handler, } if err := r.reopenLocked(); err != nil { return nil, err } return r, nil } // Handle processes a log entry, rotating output if necessary. // Thread-safe: can be called concurrently. // // Example: // // rotator.Handle(&lx.Entry{ // Level: lx.InfoLevel, // Message: "Processing request", // Namespace: "api", // }) func (r *Rotating[H]) Handle(e *lx.Entry) error { r.mu.Lock() defer r.mu.Unlock() if err := r.rotateIfNeededLocked(); err != nil { return err } return r.handler.Handle(e) } // Close releases resources (closes the current output). // Safe to call multiple times. // // Example: // // defer rotator.Close() func (r *Rotating[H]) Close() error { r.mu.Lock() defer r.mu.Unlock() if r.out != nil { return r.out.Close() } return nil } // Written returns the total bytes written to the current output destination. // Useful for metrics and monitoring. func (r *Rotating[H]) Written() int64 { r.mu.Lock() out := r.out r.mu.Unlock() return out.writtenBytes() } // rotateIfNeededLocked checks current size and rotates if maxSize exceeded. // Called with mu already held. // // The old trackingWriter is simply dereferenced (not closed) because ownership // of the underlying io.WriteCloser belongs to the src.Rotate callback. That // callback is responsible for closing, renaming, or otherwise finishing with // the old destination before src.Open is called to provide a fresh one. This // design avoids double-closes on shared writers (e.g. test mocks, pipes) and // correctly models real file-rotation where the OS rename is done before the // old fd is released. func (r *Rotating[H]) rotateIfNeededLocked() error { if r.maxSize <= 0 || r.src.Open == nil { return nil } // PERFORMANCE OPTIMIZATION: // Instead of calling r.src.Size() (which executes a slow os.Stat filesystem call), // we simply check our fast, in-memory integer counter. if r.out != nil && r.out.writtenBytes() < r.maxSize { return nil } // Drop the reference to the old trackingWriter without closing the underlying // WriteCloser. Closing/renaming is the responsibility of src.Rotate (see doc above). r.out = nil // Run rotation hook (rename/move/compress/close old file, etc.) if r.src.Rotate != nil { if err := r.src.Rotate(); err != nil { return err } } // Open fresh output return r.reopenLocked() } // reopenLocked opens a new destination and sets it on the handler. // Called with mu already held. func (r *Rotating[H]) reopenLocked() error { out, err := r.src.Open() if err != nil { return err } // We only ask the filesystem for the true file size ONCE when we first open the file. // This is necessary to know the starting size if we are appending to an existing log file. var initialSize int64 if r.src.Size != nil { initialSize, _ = r.src.Size() } // Wrap the returned io.WriteCloser so we can track all future bytes written in memory. r.out = &trackingWriter{ WriteCloser: out, written: initialSize, } r.handler.Output(r.out) return nil } golang-github-olekukonko-ll-0.1.8/lh/rotate_test.go000066400000000000000000000405101516152337300223300ustar00rootroot00000000000000package lh import ( "bytes" "errors" "fmt" "io" "strings" "sync" "sync/atomic" "testing" "github.com/olekukonko/ll/lx" ) // mockWriteCloser is a test double for io.WriteCloser type mockWriteCloser struct { mu sync.Mutex buf bytes.Buffer closed bool writeErr error closeErr error } func (m *mockWriteCloser) Write(p []byte) (n int, err error) { m.mu.Lock() defer m.mu.Unlock() if m.closed { return 0, errors.New("write to closed writer") } if m.writeErr != nil { return 0, m.writeErr } return m.buf.Write(p) } func (m *mockWriteCloser) Close() error { m.mu.Lock() defer m.mu.Unlock() if m.closeErr != nil { return m.closeErr } m.closed = true return nil } func (m *mockWriteCloser) String() string { m.mu.Lock() defer m.mu.Unlock() return m.buf.String() } // mockHandler implements both lx.Handler and lx.Outputter type mockHandler struct { mu sync.Mutex output io.Writer entries []*lx.Entry handleErr error } func (m *mockHandler) Handle(e *lx.Entry) error { if m.handleErr != nil { return m.handleErr } m.mu.Lock() m.entries = append(m.entries, e) m.mu.Unlock() if m.output != nil { data := NewJSONHandler(m.output).Handle(e) _ = data } return nil } func (m *mockHandler) Output(w io.Writer) { m.mu.Lock() m.output = w m.mu.Unlock() } func (m *mockHandler) Entries() []*lx.Entry { m.mu.Lock() defer m.mu.Unlock() out := make([]*lx.Entry, len(m.entries)) copy(out, m.entries) return out } // TestNewRotating_Basic tests basic creation and initial open func TestNewRotating_Basic(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 0, nil }, } r, err := NewRotating(handler, 1024, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() if r.out == nil { t.Fatal("expected out to be set") } if r.out.writtenBytes() != 0 { t.Fatalf("expected initial written to be 0, got %d", r.out.writtenBytes()) } } // TestNewRotating_OpenError tests error handling when Open fails func TestNewRotating_OpenError(t *testing.T) { handler := &mockHandler{} expectedErr := errors.New("open failed") src := RotateSource{ Open: func() (io.WriteCloser, error) { return nil, expectedErr }, } _, err := NewRotating(handler, 1024, src) if err != expectedErr { t.Fatalf("expected error %v, got %v", expectedErr, err) } } // TestNewRotating_ExistingFileSize tests that existing file size is preserved func TestNewRotating_ExistingFileSize(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 500, nil }, } r, err := NewRotating(handler, 1024, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() if r.out.writtenBytes() != 500 { t.Fatalf("expected initial written to be 500, got %d", r.out.writtenBytes()) } } // TestRotating_Handle_BelowMaxSize tests writing below rotation threshold func TestRotating_Handle_BelowMaxSize(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} openCount := int32(0) src := RotateSource{ Open: func() (io.WriteCloser, error) { atomic.AddInt32(&openCount, 1) return mockOut, nil }, Size: func() (int64, error) { return 0, nil }, Rotate: func() error { t.Fatal("Rotate should not be called") return nil }, } r, err := NewRotating(handler, 1000, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() entry := &lx.Entry{ Level: lx.LevelInfo, Message: "test message", } if err := r.Handle(entry); err != nil { t.Fatalf("Handle failed: %v", err) } if atomic.LoadInt32(&openCount) != 1 { t.Fatalf("expected Open to be called once, got %d", openCount) } } // TestRotating_Handle_TriggersRotation tests rotation when maxSize exceeded func TestRotating_Handle_TriggersRotation(t *testing.T) { var outputs []*mockWriteCloser handler := &mockHandler{} rotateCount := int32(0) src := RotateSource{ Open: func() (io.WriteCloser, error) { m := &mockWriteCloser{} outputs = append(outputs, m) return m, nil }, Size: func() (int64, error) { return 0, nil }, Rotate: func() error { atomic.AddInt32(&rotateCount, 1) return nil }, } r, err := NewRotating(handler, 100, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() for i := 0; i < 10; i++ { entry := &lx.Entry{ Level: lx.LevelInfo, Message: strings.Repeat("x", 20), } if err := r.Handle(entry); err != nil { t.Fatalf("Handle failed: %v", err) } } if atomic.LoadInt32(&rotateCount) < 1 { t.Fatal("expected Rotate to be called at least once") } if len(outputs) < 2 { t.Fatalf("expected multiple outputs, got %d", len(outputs)) } } // TestRotating_Handle_RotateError tests error handling when Rotate fails func TestRotating_Handle_RotateError(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} rotateErr := errors.New("rotate failed") src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 1000, nil }, Rotate: func() error { return rotateErr }, } r, err := NewRotating(handler, 100, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() entry := &lx.Entry{ Level: lx.LevelInfo, Message: "test", } err = r.Handle(entry) if err != rotateErr { t.Fatalf("expected error %v, got %v", rotateErr, err) } } // TestRotating_Handle_OpenErrorDuringRotation tests error when reopening after rotation func TestRotating_Handle_OpenErrorDuringRotation(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} openCount := 0 openErr := errors.New("reopen failed") src := RotateSource{ Open: func() (io.WriteCloser, error) { openCount++ if openCount == 1 { return mockOut, nil } return nil, openErr }, Size: func() (int64, error) { return 1000, nil }, Rotate: func() error { return nil }, } r, err := NewRotating(handler, 100, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() entry := &lx.Entry{ Level: lx.LevelInfo, Message: "test", } err = r.Handle(entry) if err != openErr { t.Fatalf("expected error %v, got %v", openErr, err) } } // TestRotating_Handle_HandlerError tests that handler errors are propagated func TestRotating_Handle_HandlerError(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} handler.handleErr = errors.New("handler failed") src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 0, nil }, } r, err := NewRotating(handler, 1024, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() entry := &lx.Entry{ Level: lx.LevelInfo, Message: "test", } err = r.Handle(entry) if err != handler.handleErr { t.Fatalf("expected error %v, got %v", handler.handleErr, err) } } // TestRotating_Close tests proper cleanup func TestRotating_Close(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 0, nil }, } r, err := NewRotating(handler, 1024, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } if err := r.Close(); err != nil { t.Fatalf("Close failed: %v", err) } if !mockOut.closed { t.Fatal("expected underlying writer to be closed") } if err := r.Close(); err != nil { t.Fatalf("Double close failed: %v", err) } } // TestRotating_CloseError tests error propagation from close func TestRotating_CloseError(t *testing.T) { mockOut := &mockWriteCloser{} mockOut.closeErr = errors.New("close failed") handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 0, nil }, } r, err := NewRotating(handler, 1024, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } err = r.Close() if err != mockOut.closeErr { t.Fatalf("expected error %v, got %v", mockOut.closeErr, err) } } // TestRotating_Written tests the Written() method func TestRotating_Written(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 100, nil }, } r, err := NewRotating(handler, 1024, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() if r.Written() != 100 { t.Fatalf("expected Written() to return 100, got %d", r.Written()) } } // TestRotating_Written_NilOutput tests Written() when output is nil func TestRotating_Written_NilOutput(t *testing.T) { handler := &mockHandler{} r := &Rotating[*mockHandler]{ maxSize: 1024, handler: handler, out: nil, } if r.Written() != 0 { t.Fatalf("expected Written() to return 0 when out is nil, got %d", r.Written()) } } // TestRotating_DisabledRotation tests that maxSize <= 0 disables rotation func TestRotating_DisabledRotation(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} rotateCalled := false src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 999999, nil }, Rotate: func() error { rotateCalled = true return nil }, } r, err := NewRotating(handler, 0, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() entry := &lx.Entry{ Level: lx.LevelInfo, Message: strings.Repeat("x", 1000), } if err := r.Handle(entry); err != nil { t.Fatalf("Handle failed: %v", err) } if rotateCalled { t.Fatal("Rotate should not be called when maxSize <= 0") } } // TestRotating_NoOpenCallback tests behavior when Open is nil func TestRotating_NoOpenCallback(t *testing.T) { handler := &mockHandler{} src := RotateSource{ Open: nil, Size: func() (int64, error) { return 0, nil }, } _, err := NewRotating(handler, 1024, src) if err == nil { t.Fatal("expected error when Open is nil") } } // TestRotating_NoSizeCallback tests rotation when Size is nil func TestRotating_NoSizeCallback(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: nil, } r, err := NewRotating(handler, 1024, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() if r.out.writtenBytes() != 0 { t.Fatalf("expected written to be 0 when Size is nil, got %d", r.out.writtenBytes()) } } // TestRotating_SizeError tests handling of Size() error func TestRotating_SizeError(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 0, errors.New("stat failed") }, } r, err := NewRotating(handler, 1024, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() if r.out.writtenBytes() != 0 { t.Fatalf("expected written to be 0 on Size error, got %d", r.out.writtenBytes()) } } // TestRotating_ConcurrentAccess tests thread safety func TestRotating_ConcurrentAccess(t *testing.T) { mockOut := &mockWriteCloser{} handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 0, nil }, } r, err := NewRotating(handler, 100000, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() var wg sync.WaitGroup numGoroutines := 100 numEntries := 100 for i := 0; i < numGoroutines; i++ { wg.Add(1) go func(id int) { defer wg.Done() for j := 0; j < numEntries; j++ { entry := &lx.Entry{ Level: lx.LevelInfo, Message: fmt.Sprintf("goroutine %d entry %d", id, j), } if err := r.Handle(entry); err != nil { t.Errorf("Handle failed: %v", err) } } }(i) } wg.Wait() entries := handler.Entries() expected := numGoroutines * numEntries if len(entries) != expected { t.Fatalf("expected %d entries, got %d", expected, len(entries)) } written := r.Written() if written <= 0 { t.Fatalf("expected positive written bytes, got %d", written) } } // TestRotating_ConcurrentWithRotation tests thread safety during rotation func TestRotating_ConcurrentWithRotation(t *testing.T) { var outputs []*mockWriteCloser var mu sync.Mutex handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { mu.Lock() defer mu.Unlock() m := &mockWriteCloser{} outputs = append(outputs, m) return m, nil }, Size: func() (int64, error) { return 0, nil }, Rotate: func() error { return nil }, } r, err := NewRotating(handler, 500, src) if err != nil { t.Fatalf("NewRotating failed: %v", err) } defer r.Close() var wg sync.WaitGroup numGoroutines := 10 for i := 0; i < numGoroutines; i++ { wg.Add(1) go func(id int) { defer wg.Done() for j := 0; j < 20; j++ { entry := &lx.Entry{ Level: lx.LevelInfo, Message: strings.Repeat(fmt.Sprintf("g%d-e%d", id, j), 50), } if err := r.Handle(entry); err != nil { t.Errorf("Handle failed: %v", err) } } }(i) } wg.Wait() mu.Lock() numOutputs := len(outputs) mu.Unlock() if numOutputs < 2 { t.Fatalf("expected multiple outputs due to rotation, got %d", numOutputs) } } // TestTrackingWriter_WriteError tests that errors don't affect written count func TestTrackingWriter_WriteError(t *testing.T) { mockOut := &mockWriteCloser{} mockOut.writeErr = errors.New("write failed") tw := &trackingWriter{ WriteCloser: mockOut, written: 0, } n, err := tw.Write([]byte("test")) if err != mockOut.writeErr { t.Fatalf("expected error %v, got %v", mockOut.writeErr, err) } if n != 0 { t.Fatalf("expected 0 bytes written on error, got %d", n) } if tw.writtenBytes() != 0 { t.Fatalf("expected written to remain 0 on error, got %d", tw.writtenBytes()) } } // TestTrackingWriter_PartialWrite tests partial write handling func TestTrackingWriter_PartialWrite(t *testing.T) { partialWriter := &partialWriteCloser{ maxWrite: 5, } tw := &trackingWriter{ WriteCloser: partialWriter, written: 0, } data := []byte("hello world") n, err := tw.Write(data) if err != nil { t.Fatalf("unexpected error: %v", err) } if n != 5 { t.Fatalf("expected 5 bytes written, got %d", n) } if tw.writtenBytes() != 5 { t.Fatalf("expected written to be 5, got %d", tw.writtenBytes()) } } // partialWriteCloser simulates a writer that only writes partial data type partialWriteCloser struct { buf bytes.Buffer maxWrite int closed bool } func (p *partialWriteCloser) Write(data []byte) (n int, err error) { if p.closed { return 0, errors.New("closed") } toWrite := len(data) if toWrite > p.maxWrite { toWrite = p.maxWrite } return p.buf.Write(data[:toWrite]) } func (p *partialWriteCloser) Close() error { p.closed = true return nil } // BenchmarkRotating_Handle benchmarks the Handle method func BenchmarkRotating_Handle(b *testing.B) { mockOut := &mockWriteCloser{} handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { return mockOut, nil }, Size: func() (int64, error) { return 0, nil }, } r, _ := NewRotating(handler, 1<<30, src) defer r.Close() entry := &lx.Entry{ Level: lx.LevelInfo, Message: "benchmark message", } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { r.Handle(entry) } }) } // BenchmarkRotating_HandleWithRotation benchmarks rotation overhead func BenchmarkRotating_HandleWithRotation(b *testing.B) { var outputs []io.WriteCloser handler := &mockHandler{} src := RotateSource{ Open: func() (io.WriteCloser, error) { m := &mockWriteCloser{} outputs = append(outputs, m) return m, nil }, Size: func() (int64, error) { return 0, nil }, Rotate: func() error { return nil }, } r, _ := NewRotating(handler, 1000, src) defer r.Close() entry := &lx.Entry{ Level: lx.LevelInfo, Message: strings.Repeat("x", 100), } b.ResetTimer() for i := 0; i < b.N; i++ { r.Handle(entry) } } golang-github-olekukonko-ll-0.1.8/lh/slog.go000066400000000000000000000071041516152337300207410ustar00rootroot00000000000000package lh import ( "context" "log/slog" "github.com/olekukonko/ll/lx" ) // 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 // // 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 in order (preserving insertion order) for _, pair := range e.Fields { record.AddAttrs(slog.Any(pair.Key, pair.Value)) // 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, lx.LevelFatal: return slog.LevelError default: return slog.LevelInfo // Default for unknown levels } } golang-github-olekukonko-ll-0.1.8/lh/text.go000066400000000000000000000152741516152337300207700ustar00rootroot00000000000000package lh import ( "bytes" "io" "strings" "sync" "time" "github.com/olekukonko/ll/lx" ) type TextOption func(*TextHandler) var textBufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } // WithTextTimeFormat enables timestamp display and optionally sets a custom time format. // It configures the TextHandler to include temporal information in each log entry, // allowing for precise tracking of when log events occur. // If the format string is empty, it defaults to time.RFC3339. func WithTextTimeFormat(format string) TextOption { return func(t *TextHandler) { t.Timestamped(true, format) } } // WithTextShowTime enables or disables timestamp display in log entries. // This option provides direct control over the visibility of the time prefix // without altering the underlying time format configured in the handler. // Setting show to true will prepend timestamps to all subsequent regular log outputs. func WithTextShowTime(show bool) TextOption { return func(t *TextHandler) { t.showTime = show } } // 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 { writer io.Writer // Destination for formatted log output showTime bool // Whether to display timestamps timeFormat string // Format for timestamps (defaults to time.RFC3339) mu sync.Mutex } // 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, opts ...TextOption) *TextHandler { t := &TextHandler{ writer: w, showTime: false, timeFormat: time.RFC3339, } for _, opt := range opts { opt(t) } return t } // 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] } } // Output sets a new writer for the TextHandler. // Thread-safe - safe for concurrent use. func (h *TextHandler) Output(w io.Writer) { h.mu.Lock() defer h.mu.Unlock() h.writer = w } // 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 { h.mu.Lock() defer h.mu.Unlock() if e.Class == lx.ClassDump { return h.handleDumpOutput(e) } if e.Class == lx.ClassRaw { _, err := h.writer.Write([]byte(e.Message)) return err } 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 { buf := textBufPool.Get().(*bytes.Buffer) buf.Reset() defer textBufPool.Put(buf) if h.showTime { buf.WriteString(e.Timestamp.Format(h.timeFormat)) buf.WriteString(lx.Space) } switch e.Style { case lx.NestedPath: if e.Namespace != "" { parts := strings.Split(e.Namespace, lx.Slash) for i, part := range parts { buf.WriteString(lx.LeftBracket) buf.WriteString(part) buf.WriteString(lx.RightBracket) if i < len(parts)-1 { buf.WriteString(lx.Arrow) } } buf.WriteString(lx.Colon) buf.WriteString(lx.Space) } default: // FlatPath if e.Namespace != "" { buf.WriteString(lx.LeftBracket) buf.WriteString(e.Namespace) buf.WriteString(lx.RightBracket) buf.WriteString(lx.Space) } } buf.WriteString(e.Level.Name(e.Class)) // buf.WriteString(lx.Space) buf.WriteString(lx.Colon) buf.WriteString(lx.Space) buf.WriteString(e.Message) if len(e.Fields) > 0 { buf.WriteString(lx.Space) buf.WriteString(lx.LeftBracket) for i, pair := range e.Fields { if i > 0 { buf.WriteString(lx.Space) } buf.WriteString(pair.Key) buf.WriteString("=") writeFieldValue(buf, pair.Value) } buf.WriteString(lx.RightBracket) } if len(e.Stack) > 0 { h.formatStack(buf, e.Stack) } if e.Level != lx.LevelNone { buf.WriteString(lx.Newline) } _, err := h.writer.Write(buf.Bytes()) 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 { buf := textBufPool.Get().(*bytes.Buffer) buf.Reset() defer textBufPool.Put(buf) if h.showTime { buf.WriteString(e.Timestamp.Format(h.timeFormat)) buf.WriteString(lx.Newline) } buf.WriteString("---- BEGIN DUMP ----\n") buf.WriteString(e.Message) buf.WriteString("---- END DUMP ----\n\n") _, err := h.writer.Write(buf.Bytes()) 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 *bytes.Buffer, stack []byte) { lines := strings.Split(string(stack), "\n") if len(lines) == 0 { return } b.WriteString("\n[stack]\n") b.WriteString(" ┌─ ") b.WriteString(lines[0]) b.WriteString("\n") for i := 1; i < len(lines); i++ { line := strings.TrimSpace(lines[i]) if line == "" { continue } if strings.Contains(line, ".go") { b.WriteString(" ├ ") } else { b.WriteString(" │ ") } b.WriteString(line) b.WriteString("\n") } b.WriteString(" └\n") } golang-github-olekukonko-ll-0.1.8/ll.go000066400000000000000000001324471516152337300200120ustar00rootroot00000000000000package ll import ( "encoding/binary" "encoding/json" "fmt" "io" "math" "os" "reflect" "runtime" "strings" "sync" "sync/atomic" "time" "github.com/olekukonko/cat" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" ) // stackBufPool pools buffers for stack trace capture to reduce allocations. var ( stackBufPool = sync.Pool{ New: func() any { return make([]byte, 4096) }, } entryPool = sync.Pool{ New: func() any { return &lx.Entry{ Fields: make(lx.Fields, 0, 4), Stack: nil, } }, } fieldsSlicePool = sync.Pool{ New: func() any { s := make(lx.Fields, 0, 8) return &s }, } ) // 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 atomic.Int32 // Determines if logging is enabled suspend atomic.Bool // uses suspend path for most actions eg. skipping namespace checks level lx.LevelType // Minimum log level (e.g., Debug, Info, Warn, Error) atomicLevel int32 // Shadow copy of level for lock-free checks namespaces *lx.Namespace // Manages namespace enable/disable states currentPath string // Current namespace path (e.g., "parent/child") context lx.Fields // 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 fatalExits bool fatalStack bool labels atomic.Pointer[[]string] } // 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: 0, // Defaults to disabled (false) level: lx.LevelDebug, // Default minimum log level atomicLevel: int32(lx.LevelDebug), // Initialize atomic level namespaces: defaultStore, // Shared namespace store currentPath: namespace, // Initial namespace path context: make(lx.Fields, 0, 10), // 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 ("/") } logger.enabled.Store(lx.Active) // Apply provided configuration options for _, opt := range opts { opt(logger) } return logger } // Apply applies one or more functional options to the default/global logger. // Useful for late configuration (e.g., after migration, attach VictoriaLogs handler, // set level, add middleware, etc.) without changing existing New() calls. // // Example: // // // In main() or init(), after setting up handler // ll.Apply( // ll.Handler(vlBatched), // ll.Level(ll.LevelInfo), // ll.Use(rateLimiterMiddleware), // ) // // Returns the default logger for chaining (if needed). func (l *Logger) Apply(opts ...Option) *Logger { l.mu.Lock() defer l.mu.Unlock() for _, opt := range opts { if opt != nil { opt(l) } } return l } // AddContext adds one or more key-value pairs to the logger's persistent context. // These fields will be included in **every** subsequent log message from this logger // (and its child namespace loggers). // // It supports variadic key-value pairs (string key, any value). // Non-string keys or uneven number of arguments will be safely ignored/logged. // // Returns the logger for chaining. // // Examples: // // logger.AddContext("user", "alice", "env", "prod") // logger.AddContext("request_id", reqID, "trace_id", traceID) // logger.AddContext("service", "payment") // single pair func (l *Logger) AddContext(pairs ...any) *Logger { l.mu.Lock() defer l.mu.Unlock() if l.context == nil { l.context = make(lx.Fields, 0, len(pairs)/2) } for i := 0; i < len(pairs)-1; i += 2 { if key, ok := pairs[i].(string); ok { l.context = append(l.context, lx.Field{Key: key, Value: pairs[i+1]}) } } 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( "duration_ms", duration.Milliseconds(), "duration", duration.String(), ).Infof("benchmark completed") 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("Inactive middleware") // Output: [app] INFO: Inactive 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 atomicLevel: l.atomicLevel, // Copy atomic level namespaces: l.namespaces, // Share namespace store currentPath: l.currentPath, // Copy namespace path context: nil, // Fresh context map (nil saves allocation, handled by AddContext) 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, atomicLevel: l.atomicLevel, namespaces: l.namespaces, currentPath: l.currentPath, context: make(lx.Fields, 0, len(l.context)+len(fields)), style: l.style, handler: l.handler, middleware: l.middleware, prefix: l.prefix, indent: l.indent, stackBufferSize: l.stackBufferSize, separator: l.separator, suspend: l.suspend, fatalExits: l.fatalExits, fatalStack: l.fatalStack, } // Copy parent's context fields (in order) newLogger.context = append(newLogger.context, l.context...) // Add new fields from map for k, v := range fields { newLogger.context = append(newLogger.context, lx.Field{Key: k, Value: v}) } return newLogger } // 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) { if l.suspend.Load() { return } // Skip logging if Debug level is not enabled if !l.shouldLog(lx.LevelDebug) { return } l.log(lx.LevelDebug, lx.ClassText, cat.Space(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.Load() { 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") // Inactive output func (l *Logger) Disable() *Logger { l.enabled.Store(lx.Inactive) 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{}) { if l.suspend.Load() { return } // 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) } } // Output logs each value as pretty-printed JSON for REST debugging. // Each value is logged on its own line with [file:line] and a blank line after the header. // Ideal for inspecting outgoing/incoming REST payloads. func (l *Logger) Output(values ...interface{}) { if l.suspend.Load() { return } l.output(2, values...) } // mark logs the caller's file and line number along with an optional custom name label for tracing execution flow. func (l *Logger) output(skip int, values ...interface{}) { if l.suspend.Load() { return } if !l.shouldLog(lx.LevelInfo) { return } _, file, line, ok := runtime.Caller(skip) if !ok { return } shortFile := file if idx := strings.LastIndex(file, "/"); idx >= 0 { shortFile = file[idx+1:] } header := fmt.Sprintf("[%s:%d] JSON:\n", shortFile, line) for _, v := range values { // Always pretty-print with indent b, err := json.MarshalIndent(v, " ", " ") if err != nil { b, _ = json.MarshalIndent(map[string]any{ "value": fmt.Sprintf("%+v", v), "error": err.Error(), }, " ", " ") } l.log(lx.LevelInfo, lx.ClassOutput, header+string(b), nil, false) } } // Inspect logs one or more values in a **developer-friendly, deeply introspective format** at Info level. // It includes the caller file and line number, and reveals **all fields** â including: // // - Private (unexported) fields â prefixed with `(field)` // - Embedded structs (inlined) // - Pointers and nil values â shown as `*(field)` or `nil` // - Full struct nesting and type information // // This method uses `NewInspector` under the hood, which performs **full reflection-based traversal**. // It is **not** meant for production logging or REST APIs â use `Output` for that. // // Ideal for: // - Debugging complex internal state // - Inspecting structs with private fields // - Understanding struct embedding and pointer behavior func (l *Logger) Inspect(values ...interface{}) { if l.suspend.Load() { return } o := NewInspector(l) o.Log(2, values...) } // 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.enabled.Store(lx.Active) 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 { return l.enabled.Load() == lx.Active } // 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) { if l.suspend.Load() { return } // Skip logging if Error level is not enabled if !l.shouldLog(lx.LevelError) { return } l.mu.Lock() defer l.mu.Unlock() // Initialize context slice if nil if l.context == nil { l.context = make(lx.Fields, 0, 4) } // 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 = append(l.context, lx.Field{Key: "error", Value: nonNilErrors[0]}) } else { // Store slice of errors l.context = append(l.context, lx.Field{Key: "error", Value: nonNilErrors}) } // Log concatenated error messages l.log(lx.LevelError, lx.ClassText, builder.String(), nil, false) } } // 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) { if l.suspend.Load() { return } // Skip logging if Error level is not enabled if !l.shouldLog(lx.LevelError) { return } l.log(lx.LevelError, lx.ClassText, cat.Space(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.Load() { 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) { if l.suspend.Load() { return } if !l.shouldLog(lx.LevelError) { os.Exit(1) } l.log(lx.LevelFatal, lx.ClassText, cat.Space(args...), nil, l.fatalStack) if l.fatalExits { 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) { if l.suspend.Load() { return } l.Fatal(fmt.Sprintf(format, args...)) } // FieldOne logs a message at Error level with a stack trace and exits the program with // exit code 1. It is thread-safe. func (l *Logger) FieldOne(key string, value any) *FieldBuilder { fb := fieldBuilderPool.Get().(*FieldBuilder) fb.logger = l fb.fields = fb.fields[:1] fb.fields[0] = lx.Field{Key: key, Value: value} return fb } // FieldSet avoids variadic allocation overhead by accepting a slice of strongly typed fields. // Ideally, lx.Field is struct{Key string, Value any} func (l *Logger) FieldSet(fields []lx.Field) *FieldBuilder { fb := getFieldBuilder(l, len(fields)) if l.suspend.Load() { return fb } fb.fields = append(fb.fields, fields...) return fb } // 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] // // Field starts a fluent chain for adding fields from a map func (l *Logger) Field(fields map[string]interface{}) *FieldBuilder { fb := getFieldBuilder(l, len(fields)) if l.suspend.Load() { return fb } for k, v := range fields { fb.fields = append(fb.fields, lx.Field{Key: k, Value: v}) } return fb } // Fields starts a fluent chain for adding fields using variadic key-value pairs. // 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: // // logger.Fields("user", "alice").Info("Action") // Output: [app] INFO: Action [user=alice] func (l *Logger) Fields(pairs ...any) *FieldBuilder { fb := getFieldBuilder(l, len(pairs)/2) if l.suspend.Load() { return fb } // Process key-value pairs for i := 0; i < len(pairs)-1; i += 2 { if key, ok := pairs[i].(string); ok { fb.fields = append(fb.fields, lx.Field{Key: key, Value: pairs[i+1]}) } else { // Log error for non-string keys fb.fields = append(fb.fields, lx.Field{ Key: "error", Value: fmt.Errorf("non-string key in Fields: %v", pairs[i]), }) } } // Log error for uneven pairs if len(pairs)%2 != 0 { fb.fields = append(fb.fields, lx.Field{ Key: "error", Value: 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() // Convert slice to map for backward compatibility contextMap := make(map[string]interface{}, len(l.context)) for _, pair := range l.context { contextMap[pair.Key] = pair.Value } return contextMap } // 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.Load() { return } if !l.shouldLog(lx.LevelInfo) { return } l.log(lx.LevelInfo, lx.ClassText, cat.Space(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.Load() { 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() } // Labels temporarily attaches one or more label names to the logger for the next log entry. // Labels are typically used for metrics, benchmarking, tracing, or categorizing logs in a structured way. // // The labels are stored atomically and intended to be short-lived, applying only to the next // log operation (or until overwritten by a subsequent call to Labels). Multiple labels can // be provided as separate string arguments. // // Example usage: // // logger := New("app").Enable() // // // Add labels for a specific operation // logger.Labels("load_users", "process_orders").Measure(func() { // // ... perform work ... // }, func() { // // ... optional callback ... // }) func (l *Logger) Labels(names ...string) *Logger { l.labels.Store(&names) // store temporarily return l } // 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") // Inactive 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 atomic.StoreInt32(&l.atomicLevel, int32(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) { if l.suspend.Load() { return } l.mark(2, name...) } // mark logs the caller's file and line number along with an optional custom name label for tracing execution flow. func (l *Logger) mark(skip int, names ...string) { if l.suspend.Load() { return } // 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) } // 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.Load() { 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, atomicLevel: l.atomicLevel, namespaces: l.namespaces, currentPath: fullPath, context: nil, // Fresh context map (nil saves allocation) 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") // Inactive 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 { fullPath := l.joinPath(l.currentPath, relativePath) separator := l.separator if separator == "" { separator = lx.Slash } // Handle root path case if fullPath == "" && relativePath == "" { return l.enabled.Load() == lx.Active } 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 l.enabled.Load() == lx.Active } // 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 := cat.Space(args...) if l.suspend.Load() { panic(msg) } // Panic immediately if Error level is not enabled if !l.shouldLog(lx.LevelError) { panic(msg) } l.log(lx.LevelFatal, 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.Load() { return } // Skip logging if Info level is not enabled if !l.shouldLog(lx.LevelInfo) { return } l.log(lx.LevelNone, lx.ClassRaw, cat.Space(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.Load() { return } // Skip logging if Info level is not enabled if !l.shouldLog(lx.LevelInfo) { return } l.log(lx.LevelNone, lx.ClassRaw, cat.SuffixWith(lx.Space, 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.Load() { 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.suspend.Store(false) 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") // Inactive output func (l *Logger) Suspend() *Logger { l.suspend.Store(true) 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 { return l.suspend.Load() } // 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) { for _, arg := range args { l.log(lx.LevelError, lx.ClassStack, cat.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) { 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 } // Toggle enables or disables the logger based on the provided boolean value and returns the updated logger instance. func (l *Logger) Toggle(v bool) *Logger { if v { l.Resume() return l.Enable() } l.Suspend() return l.Disable() } // 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") // Inactive 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.Load() { return } // Skip logging if Warn level is not enabled if !l.shouldLog(lx.LevelWarn) { return } l.log(lx.LevelWarn, lx.ClassText, cat.Space(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.Load() { return } l.Warn(fmt.Sprintf(format, args...)) } // 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 cat.Concat(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) // // 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. func (l *Logger) log(level lx.LevelType, class lx.ClassType, msg string, fields lx.Fields, withStack bool) { // Skip logging if level is not enabled (fast path) if !l.shouldLog(level) { return } var stack []byte // Capture stack trace if requested (outside lock) if withStack { l.mu.RLock() size := l.stackBufferSize l.mu.RUnlock() buf := stackBufPool.Get().([]byte) if cap(buf) < size { buf = make([]byte, size) } else { buf = buf[:size] } n := runtime.Stack(buf, false) stack = append([]byte(nil), buf[:n]...) stackBufPool.Put(buf) } // Read-only config snapshot (minimal lock scope) l.mu.RLock() handler := l.handler prefix := l.prefix indent := l.indent context := l.context style := l.style currentPath := l.currentPath middleware := l.middleware l.mu.RUnlock() // Apply prefix and indentation to the message (outside lock) var builder strings.Builder // Optimization: Pre-grow buffer if indent/prefix known if indent > 0 { builder.Grow(indent*2 + len(prefix) + len(msg)) builder.WriteString(strings.Repeat(lx.DoubleSpace, indent)) } else { builder.Grow(len(prefix) + len(msg)) } if prefix != "" { builder.WriteString(prefix) } builder.WriteString(msg) finalMsg := builder.String() // Optimized field merging - avoid allocation when possible var combinedFields lx.Fields var pooledFields *lx.Fields // Track if we allocated from pool if len(context) == 0 { combinedFields = fields } else if len(fields) == 0 { combinedFields = context } else { // Get pooled slice pooledFields = fieldsSlicePool.Get().(*lx.Fields) combinedFields = (*pooledFields)[:0] // Reset length, keep capacity combinedFields = append(combinedFields, context...) combinedFields = append(combinedFields, fields...) } // Get entry from pool entry := entryPool.Get().(*lx.Entry) // Ensure pool return on ALL paths (including middleware errors) defer func() { // Reset slices to zero length but keep capacity for pool reuse entry.Fields = entry.Fields[:0] entry.Stack = entry.Stack[:0] entryPool.Put(entry) }() entry.Timestamp = time.Now() entry.Level = level entry.Message = finalMsg entry.Namespace = currentPath entry.Fields = combinedFields entry.Style = style entry.Class = class entry.Stack = stack entry.Error = nil entry.Id = 0 // Apply middleware, stopping if any returns an error for _, mw := range middleware { if err := mw.fn.Handle(entry); err != nil { // Defer handles pool return return } } // Pass to handler if set if handler != nil { _ = handler.Handle(entry) l.entries.Add(1) } // Defer handles pool return } // 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 } // Atomic fast path: read level without lock if level > lx.LevelType(atomic.LoadInt32(&l.atomicLevel)) { return false } // Check namespace rules if path is set (minimal lock scope) if l.currentPath != "" { separator := l.separator if separator == "" { separator = lx.Slash } isEnabledByNSRule, isDisabledByNSRule := l.namespaces.Enabled(l.currentPath, separator) if isDisabledByNSRule { return false } if isEnabledByNSRule { return true } } return l.enabled.Load() == lx.Active } golang-github-olekukonko-ll-0.1.8/lm/000077500000000000000000000000001516152337300174515ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/lm/rate.go000066400000000000000000000064261516152337300207430ustar00rootroot00000000000000package lm import ( "fmt" "hash/fnv" "sync" "time" "github.com/olekukonko/ll/lx" ) // shardCount determines the number of shards for the rate limiter. // Should be a power of 2 for efficient modulo operations. const shardCount = 32 // RateLimiter is a sharded middleware that limits the rate of log entries per level. type RateLimiter struct { shards [shardCount]*rateLimitShard } // rateLimitShard holds rate limiting state for a subset of log levels. type rateLimitShard struct { limits sync.Map // map[lx.LevelType]*rateLimit } // rateLimit holds rate limiting state for a specific log level. type rateLimit struct { count int32 // Current count maxCount int32 // Maximum allowed interval int64 // Interval in nanoseconds last int64 // Last update timestamp in nanoseconds mu sync.Mutex // Protects count/last during reset } // NewRateLimiter creates a new sharded RateLimiter for a specific log level. func NewRateLimiter(level lx.LevelType, count int, interval time.Duration) *RateLimiter { r := &RateLimiter{} for i := range r.shards { r.shards[i] = &rateLimitShard{} } r.Set(level, count, interval) return r } // Set configures a rate limit for a specific log level. func (rl *RateLimiter) Set(level lx.LevelType, count int, interval time.Duration) *RateLimiter { shard := rl.getShard(level) limit := &rateLimit{ count: 0, maxCount: int32(count), interval: interval.Nanoseconds(), last: time.Now().UnixNano(), } shard.limits.Store(level, limit) return rl } // getShard returns the appropriate shard for a log level using FNV hash. func (rl *RateLimiter) getShard(level lx.LevelType) *rateLimitShard { h := fnv.New32a() h.Write([]byte{byte(level)}) idx := h.Sum32() & (shardCount - 1) return rl.shards[idx] } // Handle processes a log entry and enforces rate limiting. func (rl *RateLimiter) Handle(e *lx.Entry) error { shard := rl.getShard(e.Level) // Fast path: check if limit exists without locking val, exists := shard.limits.Load(e.Level) if !exists { return nil } limit := val.(*rateLimit) now := time.Now().UnixNano() // Check if interval passed if now-limit.last >= limit.interval { limit.mu.Lock() // Double-check after acquiring lock current := time.Now().UnixNano() if current-limit.last >= limit.interval { limit.last = current limit.count = 1 limit.mu.Unlock() return nil } limit.mu.Unlock() } // Increment count and check limit limit.mu.Lock() defer limit.mu.Unlock() // Re-check interval in case another goroutine reset it current := time.Now().UnixNano() if current-limit.last >= limit.interval { limit.last = current limit.count = 1 return nil } limit.count++ if limit.count > limit.maxCount { return fmt.Errorf("rate limit exceeded for level %v", e.Level) } return nil } // Delete removes a rate limit for a specific level. func (rl *RateLimiter) Delete(level lx.LevelType) { shard := rl.getShard(level) shard.limits.Delete(level) } // Get retrieves the current rate limit settings for a level. func (rl *RateLimiter) Get(level lx.LevelType) (int, time.Duration, bool) { shard := rl.getShard(level) val, exists := shard.limits.Load(level) if !exists { return 0, 0, false } limit := val.(*rateLimit) return int(limit.maxCount), time.Duration(limit.interval), true } golang-github-olekukonko-ll-0.1.8/lm/sampling.go000066400000000000000000000070231516152337300216140ustar00rootroot00000000000000package lm import ( "fmt" "math/rand" "sync" "github.com/olekukonko/ll/lx" ) // 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: Inactive rate for level %v\n", e.Level) return nil // Inactive 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.1.8/lx/000077500000000000000000000000001516152337300174645ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/lx/field.go000066400000000000000000000076721516152337300211120ustar00rootroot00000000000000package lx import ( "fmt" "strings" ) // Field represents a key-value pair where the key is a string and the value is of any type. type Field struct { Key string Value interface{} } // Fields represents a slice of key-value pairs. type Fields []Field // Map converts the Fields slice to a map[string]interface{}. // This is useful for backward compatibility or when map operations are needed. // Example: // // fields := lx.Fields{{"user", "alice"}, {"age", 30}} // m := fields.Map() // Returns map[string]interface{}{"user": "alice", "age": 30} func (f Fields) Map() map[string]interface{} { m := make(map[string]interface{}, len(f)) for _, pair := range f { m[pair.Key] = pair.Value } return m } // Get returns the value for a given key and a boolean indicating if the key was found. // This provides O(n) lookup, which is fine for small numbers of fields. // Example: // // fields := lx.Fields{{"user", "alice"}, {"age", 30}} // value, found := fields.Get("user") // Returns "alice", true func (f Fields) Get(key string) (interface{}, bool) { for _, pair := range f { if pair.Key == key { return pair.Value, true } } return nil, false } // Filter returns a new Fields slice containing only pairs where the predicate returns true. // Example: // // fields := lx.Fields{{"user", "alice"}, {"password", "secret"}, {"age", 30}} // filtered := fields.Filter(func(key string, value interface{}) bool { // return key != "password" // Remove sensitive fields // }) func (f Fields) Filter(predicate func(key string, value interface{}) bool) Fields { result := make(Fields, 0, len(f)) for _, pair := range f { if predicate(pair.Key, pair.Value) { result = append(result, pair) } } return result } // Translate returns a new Fields slice with keys translated according to the provided mapping. // Keys not in the mapping are passed through unchanged. This is useful for adapters like Victoria. // Example: // // fields := lx.Fields{{"user", "alice"}, {"timestamp", time.Now()}} // translated := fields.Translate(map[string]string{ // "user": "username", // "timestamp": "ts", // }) // // Returns: {{"username", "alice"}, {"ts", time.Now()}} func (f Fields) Translate(mapping map[string]string) Fields { result := make(Fields, len(f)) for i, pair := range f { if newKey, ok := mapping[pair.Key]; ok { result[i] = Field{Key: newKey, Value: pair.Value} } else { result[i] = pair } } return result } // Merge merges another Fields slice into this one, with the other slice's fields taking precedence // for duplicate keys (overwrites existing keys). // Example: // // base := lx.Fields{{"user", "alice"}, {"age", 30}} // additional := lx.Fields{{"age", 31}, {"city", "NYC"}} // merged := base.Merge(additional) // // Returns: {{"user", "alice"}, {"age", 31}, {"city", "NYC"}} func (f Fields) Merge(other Fields) Fields { result := make(Fields, 0, len(f)+len(other)) // Create a map to track which keys from 'other' we've seen seen := make(map[string]bool, len(other)) // First add all fields from 'f' result = append(result, f...) // Then add fields from 'other', overwriting duplicates for _, pair := range other { // Check if this key already exists in result found := false for i, existing := range result { if existing.Key == pair.Key { result[i] = pair // Overwrite found = true break } } if !found { result = append(result, pair) } seen[pair.Key] = true } return result } // String returns a human-readable string representation of the fields. // Example: // // fields := lx.Fields{{"user", "alice"}, {"age", 30}} // str := fields.String() // Returns: "[user=alice age=30]" func (f Fields) String() string { var builder strings.Builder builder.WriteString(LeftBracket) for i, pair := range f { if i > 0 { builder.WriteString(Space) } builder.WriteString(pair.Key) builder.WriteString("=") builder.WriteString(fmt.Sprint(pair.Value)) } builder.WriteString(RightBracket) return builder.String() } golang-github-olekukonko-ll-0.1.8/lx/interface.go000066400000000000000000000051611516152337300217560ustar00rootroot00000000000000package lx import "io" // 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 } // Outputter defines the interface for handlers that support dynamic output // destination changes. Implementations can switch their output writer at runtime. // // Example usage: // // h := &JSONHandler{} // h.Output(os.Stderr) // Switch to stderr // h.Output(file) // Switch to file type Outputter interface { Output(w io.Writer) } // HandlerOutputter combines the Handler and Outputter interfaces. // Types implementing this interface can both process log entries and // dynamically change their output destination at runtime. // // This is useful for creating flexible logging handlers that support // features like log rotation, output redirection, or runtime configuration. // // Example usage: // // var ho HandlerOutputter = &TextHandler{} // // Handle log entries // ho.Handle(&Entry{...}) // // Switch output destination // ho.Output(os.Stderr) // // Common implementations include TextHandler and JSONHandler when they // support output destination changes. type HandlerOutputter interface { Handler // Can process log entries Outputter // Can change output destination (has Output(w io.Writer) method) } // 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) } // Wrap is a handler decorator function that transforms a log handler. // It takes an existing handler as input and returns a new, wrapped handler // that adds functionality (like filtering, transformation, or routing). type Wrap func(next Handler) Handler // Deduper defines how to calculate a deduplication key for an entry. type Deduper interface { Calculate(*Entry) uint64 } golang-github-olekukonko-ll-0.1.8/lx/lx.go000066400000000000000000000077711516152337300204520ustar00rootroot00000000000000package lx // 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:) can also be "|" 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 = true // Default state for new loggers (disabled) True = true False = false Active = 1 Inactive = -1 Unknown = 0 ) // 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 LevelFatal // Fatal level for critical error conditions LevelDebug // None level for logs without a specific severity (e.g., raw output) LevelUnknown // None level for logs without a specific severity (e.g., raw output) ) // String constants for each level const ( DebugString = "DEBUG" InfoString = "INFO" WarnString = "WARN" WarningString = "WARNING" ErrorString = "ERROR" FatalString = "FATAL" NoneString = "NONE" UnknownString = "UNKNOWN" TextString = "TEXT" JSONString = "JSON" DumpString = "DUMP" SpecialString = "SPECIAL" RawString = "RAW" InspectString = "INSPECT" DbgString = "DBG" TimedString = "TIMED" StackString = "STACK" OutputString = "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 ClassInspect // Inspect entries for debugging ClassDbg // Inspect entries for debugging ClassTimed // Inspect entries for debugging ClassStack // Inspect entries for debugging ClassOutput // Inspect entries for debugging ClassUnknown // Unknown 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] ) golang-github-olekukonko-ll-0.1.8/lx/namespace.go000066400000000000000000000066441516152337300217610ustar00rootroot00000000000000package lx import ( "strings" "sync" "sync/atomic" ) // namespaceRule stores the cached result of Enabled. type namespaceRule struct { isEnabledByRule bool isDisabledByRule bool generation uint64 // NEW: track cache validity } // 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) cache sync.Map // path (string) -> namespaceRule genCounter uint64 // NEW: atomic generation counter } // 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.invalidatePathCache(path) } // invalidatePathCache increments generation counter instead of scanning cache. func (ns *Namespace) invalidatePathCache(path string) { // Atomic increment - O(1), no lock contention on cache atomic.AddUint64(&ns.genCounter, 1) } // 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 == "" { return false, false } // Check cache with generation validation if cachedValue, found := ns.cache.Load(path); found { if state, ok := cachedValue.(namespaceRule); ok { // If cache generation matches current, result is valid if state.generation == atomic.LoadUint64(&ns.genCounter) { return state.isEnabledByRule, state.isDisabledByRule } // Stale cache - fall through to recompute ns.cache.Delete(path) } } // Compute: Most specific rule wins (original logic) 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 with current generation ns.cache.Store(path, namespaceRule{ isEnabledByRule: computedIsEnabled, isDisabledByRule: computedIsDisabled, generation: atomic.LoadUint64(&ns.genCounter), }) return computedIsEnabled, computedIsDisabled } golang-github-olekukonko-ll-0.1.8/lx/types.go000066400000000000000000000111141516152337300211550ustar00rootroot00000000000000package lx import ( "strings" "time" ) // levelStrings provides O(1) lookup for level string conversion var levelStrings = [7]string{ LevelNone: NoneString, LevelInfo: InfoString, LevelWarn: WarnString, LevelError: ErrorString, LevelFatal: FatalString, LevelDebug: DebugString, LevelUnknown: UnknownString, } // 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 { if l >= 0 && int(l) < len(levelStrings) { return levelStrings[l] } return UnknownString } func (l LevelType) Name(class ClassType) string { if class == ClassRaw || class == ClassDump || class == ClassInspect || class == ClassDbg || class == ClassTimed { return class.String() } return l.String() } // LevelParse converts a string to its corresponding LevelType. // It parses a string (case-insensitive) and returns the corresponding LevelType, defaulting to // LevelUnknown for unrecognized strings. Supports "WARNING" as an alias for "WARN". func LevelParse(s string) LevelType { switch strings.ToUpper(s) { case DebugString: return LevelDebug case InfoString: return LevelInfo case WarnString, WarningString: // Allow both "WARN" and "WARNING" return LevelWarn case ErrorString: return LevelError case NoneString: return LevelNone default: return LevelUnknown } } // 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 Fields // 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 } // 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 // 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 TextString case ClassJSON: return JSONString case ClassDump: return DumpString case ClassSpecial: return SpecialString case ClassInspect: return InspectString case ClassDbg: return DbgString case ClassRaw: return RawString case ClassTimed: return TimedString case ClassStack: return StackString case ClassOutput: return OutputString default: return UnknownString } } // ParseClass converts a string to its corresponding ClassType. // It parses a string (case-insensitive) and returns the corresponding ClassType, defaulting to // ClassUnknown for unrecognized strings. func ParseClass(s string) ClassType { switch strings.ToUpper(s) { case TextString: return ClassText case JSONString: return ClassJSON case DumpString: return ClassDump case SpecialString: return ClassSpecial case RawString: return ClassRaw default: return ClassUnknown } } golang-github-olekukonko-ll-0.1.8/middleware.go000066400000000000000000000107221516152337300215070ustar00rootroot00000000000000package 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 { last := len(m.logger.middleware) - 1 m.logger.middleware[i] = m.logger.middleware[last] m.logger.middleware = m.logger.middleware[:last] 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.1.8/options.go000066400000000000000000000036531516152337300210720ustar00rootroot00000000000000package ll import ( "github.com/olekukonko/ll/lx" ) // 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 } } // Functional options (can be passed to New() or applied later) func WithFatalExits(enabled bool) Option { return func(l *Logger) { l.fatalExits = enabled } } func WithFatalStack(enabled bool) Option { return func(l *Logger) { l.fatalStack = enabled } } golang-github-olekukonko-ll-0.1.8/since.go000066400000000000000000000236771516152337300205100ustar00rootroot00000000000000package ll import ( "fmt" "strings" "time" "github.com/olekukonko/ll/lx" ) // Measure executes one or more functions and logs the duration of each. // It returns the total cumulative duration across all functions. // // Each function in `fns` is run sequentially. If a function is `nil`, it is skipped. // // Optional labels previously set via `Labels(...)` are applied to the corresponding function // by position. If there are fewer labels than functions, missing labels are replaced with // default names like "fn_0", "fn_1", etc. Labels are cleared after the call to prevent reuse. // // Example usage: // // logger := New("app").Enable() // // // Optional: add labels for functions // logger.Labels("load_users", "process_orders") // // total := logger.Measure( // func() { // // simulate work 1 // time.Sleep(100 * time.Millisecond) // }, // func() { // // simulate work 2 // time.Sleep(200 * time.Millisecond) // }, // func() { // // simulate work 3 // time.Sleep(50 * time.Millisecond) // }, // ) // // // Logs something like: // // [load_users] completed duration=100ms // // [process_orders] completed duration=200ms // // [fn_2] completed duration=50ms // // Returns the sum of durations of all executed functions. func (l *Logger) Measure(fns ...func()) time.Duration { if len(fns) == 0 { return 0 } var total time.Duration lblPtr := l.labels.Swap(nil) var lbls []string if lblPtr != nil { lbls = *lblPtr } for i, fn := range fns { if fn == nil { continue } // Use SinceBuilder instead of manual timing sb := l.Since() // starts timer internally fn() duration := sb.Fields( "index", i, ).Info(fmt.Sprintf("[%s] completed", func() string { if i < len(lbls) && lbls[i] != "" { return lbls[i] } return fmt.Sprintf("fn_%d", i) }())) total += duration } return total } // Since creates a timer that will log the duration when completed // If startTime is provided, uses that as the start time; otherwise uses time.Now() // // defer logger.Since().Info("request") // Auto-start // logger.Since(start).Info("request") // Manual timing // logger.Since().If(debug).Debug("timing") // Conditional func (l *Logger) Since(startTime ...time.Time) *SinceBuilder { start := time.Now() if len(startTime) > 0 && !startTime[0].IsZero() { start = startTime[0] } return &SinceBuilder{ logger: l, start: start, condition: true, fields: nil, // Lazily initialized } } // SinceBuilder provides a fluent API for logging timed operations // It mirrors FieldBuilder exactly for field operations type SinceBuilder struct { logger *Logger start time.Time condition bool fields lx.Fields } // --------------------------------------------------------------------- // Conditional Methods (match conditional.go pattern) // --------------------------------------------------------------------- // If adds a condition to this timer - only logs if condition is true func (sb *SinceBuilder) If(condition bool) *SinceBuilder { sb.condition = sb.condition && condition return sb } // IfErr adds an error condition - only logs if err != nil func (sb *SinceBuilder) IfErr(err error) *SinceBuilder { sb.condition = sb.condition && (err != nil) return sb } // IfAny logs if ANY condition is true func (sb *SinceBuilder) IfAny(conditions ...bool) *SinceBuilder { if !sb.condition { return sb } for _, cond := range conditions { if cond { return sb } } sb.condition = false return sb } // IfOne logs if ALL conditions are true func (sb *SinceBuilder) IfOne(conditions ...bool) *SinceBuilder { if !sb.condition { return sb } for _, cond := range conditions { if !cond { sb.condition = false return sb } } return sb } // --------------------------------------------------------------------- // Field Methods - EXACT MATCH with FieldBuilder API // --------------------------------------------------------------------- // Fields adds key-value pairs as fields (variadic) // EXACT match to FieldBuilder.Fields() func (sb *SinceBuilder) Fields(pairs ...any) *SinceBuilder { if sb.logger.suspend.Load() || !sb.condition { return sb } // Lazy initialization if sb.fields == nil { sb.fields = make(lx.Fields, 0, len(pairs)/2) } // Process key-value pairs for i := 0; i < len(pairs)-1; i += 2 { if key, ok := pairs[i].(string); ok { sb.fields = append(sb.fields, lx.Field{Key: key, Value: pairs[i+1]}) } else { // Log error for non-string keys (matches Fields behavior) sb.fields = append(sb.fields, lx.Field{ Key: "error", Value: fmt.Errorf("missing key '%v'", pairs[i]), }) } } // Handle uneven pairs (matches Fields behavior) if len(pairs)%2 != 0 { sb.fields = append(sb.fields, lx.Field{ Key: "error", Value: fmt.Errorf("missing key '%v'", pairs[len(pairs)-1]), }) } return sb } // Field adds fields from a map // EXACT match to FieldBuilder.Field() func (sb *SinceBuilder) Field(fields map[string]interface{}) *SinceBuilder { if sb.logger.suspend.Load() || !sb.condition || len(fields) == 0 { return sb } // Lazy initialization if sb.fields == nil { sb.fields = make(lx.Fields, 0, len(fields)) } // Copy fields from input map (preserves iteration order) for k, v := range fields { sb.fields = append(sb.fields, lx.Field{Key: k, Value: v}) } return sb } // Err adds one or more errors as a field // EXACT match to FieldBuilder.Err() func (sb *SinceBuilder) Err(errs ...error) *SinceBuilder { if sb.logger.suspend.Load() || !sb.condition { return sb } // Lazy initialization if sb.fields == nil { sb.fields = make(lx.Fields, 0, 2) } // Collect non-nil errors 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 { sb.fields = append(sb.fields, lx.Field{Key: "error", Value: nonNilErrors[0]}) } else { sb.fields = append(sb.fields, lx.Field{Key: "error", Value: nonNilErrors}) } // Note: Unlike FieldBuilder.Err(), we DON'T log immediately // The error will be included in the timing log } return sb } // Merge adds additional key-value pairs to the fields // EXACT match to FieldBuilder.Merge() func (sb *SinceBuilder) Merge(pairs ...any) *SinceBuilder { if sb.logger.suspend.Load() || !sb.condition { return sb } // Lazy initialization if sb.fields == nil { sb.fields = make(lx.Fields, 0, len(pairs)/2) } // Process pairs as key-value for i := 0; i < len(pairs)-1; i += 2 { if key, ok := pairs[i].(string); ok { sb.fields = append(sb.fields, lx.Field{Key: key, Value: pairs[i+1]}) } else { sb.fields = append(sb.fields, lx.Field{ Key: "error", Value: fmt.Errorf("non-string key in Merge: %v", pairs[i]), }) } } if len(pairs)%2 != 0 { sb.fields = append(sb.fields, lx.Field{ Key: "error", Value: fmt.Errorf("uneven key-value pairs in Merge: [%v]", pairs[len(pairs)-1]), }) } return sb } // --------------------------------------------------------------------- // Logging Methods (match logger pattern) // --------------------------------------------------------------------- // Debug logs the duration at Debug level with message func (sb *SinceBuilder) Debug(msg string) time.Duration { return sb.logAtLevel(lx.LevelDebug, msg) } // Info logs the duration at Info level with message func (sb *SinceBuilder) Info(msg string) time.Duration { return sb.logAtLevel(lx.LevelInfo, msg) } // Warn logs the duration at Warn level with message func (sb *SinceBuilder) Warn(msg string) time.Duration { return sb.logAtLevel(lx.LevelWarn, msg) } // Error logs the duration at Error level with message func (sb *SinceBuilder) Error(msg string) time.Duration { return sb.logAtLevel(lx.LevelError, msg) } // Log is an alias for Info (for backward compatibility) func (sb *SinceBuilder) Log(msg string) time.Duration { return sb.Info(msg) } // logAtLevel internal method that handles the actual logging func (sb *SinceBuilder) logAtLevel(level lx.LevelType, msg string) time.Duration { // Fast path - don't even compute duration if we're not logging if !sb.condition || sb.logger.suspend.Load() || !sb.logger.shouldLog(level) { return time.Since(sb.start) } duration := time.Since(sb.start) // Build final fields in this order: // 1. Logger context fields (from logger.context) // 2. Builder fields (from sb.fields) // 3. Duration fields (always last) // Pre-allocate with exact capacity totalFields := 0 if sb.logger.context != nil { totalFields += len(sb.logger.context) } if sb.fields != nil { totalFields += len(sb.fields) } totalFields += 2 // duration_ms, duration fields := make(lx.Fields, 0, totalFields) // Add logger context fields first (preserves order) if sb.logger.context != nil { fields = append(fields, sb.logger.context...) } // Add builder fields if sb.fields != nil { fields = append(fields, sb.fields...) } // Add duration fields last (so they're visible at the end) fields = append(fields, lx.Field{Key: "duration_ms", Value: duration.Milliseconds()}, lx.Field{Key: "duration", Value: duration.String()}, ) sb.logger.log(level, lx.ClassTimed, msg, fields, false) return duration } // --------------------------------------------------------------------- // Utility Methods // --------------------------------------------------------------------- // Reset allows reusing the builder with a new start time // Zero-allocation - keeps fields slice capacity func (sb *SinceBuilder) Reset(startTime ...time.Time) *SinceBuilder { sb.start = time.Now() if len(startTime) > 0 && !startTime[0].IsZero() { sb.start = startTime[0] } sb.condition = true if sb.fields != nil { sb.fields = sb.fields[:0] // Keep capacity, zero length } return sb } // Elapsed returns the current duration without logging func (sb *SinceBuilder) Elapsed() time.Duration { return time.Since(sb.start) } golang-github-olekukonko-ll-0.1.8/tests/000077500000000000000000000000001516152337300202035ustar00rootroot00000000000000golang-github-olekukonko-ll-0.1.8/tests/bench_test.go000066400000000000000000000222521516152337300226530ustar00rootroot00000000000000package tests import ( "io" "testing" "time" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" ) // Helper to create a test logger func newTestLogger(level lx.LevelType) *ll.Logger { return ll.New("app", ll.WithHandler(lh.NewTextHandler(io.Discard)), ll.WithLevel(level), ).Enable() } // BenchmarkDisabledLogger tests the cost of logging when logger is disabled func BenchmarkDisabledLogger(b *testing.B) { logger := ll.New("app").Disable() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Info("test message") } }) } // BenchmarkLevelFiltered tests filtering at different levels func BenchmarkLevelFiltered(b *testing.B) { // Level set to ERROR, trying to log INFO (should be filtered) logger := newTestLogger(lx.LevelError) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Info("filtered message") } }) } // BenchmarkSimpleInfo tests basic Info logging func BenchmarkSimpleInfo(b *testing.B) { logger := newTestLogger(lx.LevelDebug) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Info("simple message") } }) } // BenchmarkInfoWithFields tests logging with fields func BenchmarkInfoWithFields(b *testing.B) { logger := newTestLogger(lx.LevelDebug) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Fields( "user", "alice", "action", "login", "duration_ms", 42, ).Info("user action") } }) } // BenchmarkInfof tests formatted logging func BenchmarkInfof(b *testing.B) { logger := newTestLogger(lx.LevelDebug) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Infof("user %s performed %s in %dms", "alice", "login", 42) } }) } // BenchmarkNamespaceCreation tests namespace hierarchy creation func BenchmarkNamespaceCreation(b *testing.B) { root := newTestLogger(lx.LevelDebug) b.ResetTimer() for i := 0; i < b.N; i++ { child := root.Namespace("child").Namespace("grandchild") child.Info("test") } } // BenchmarkConditionalLogging tests If() conditional chains func BenchmarkConditionalLogging(b *testing.B) { logger := newTestLogger(lx.LevelDebug) condition := true b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.If(condition).Info("conditional message") } }) } // BenchmarkConditionalSkipped tests when condition is false func BenchmarkConditionalSkipped(b *testing.B) { logger := newTestLogger(lx.LevelDebug) condition := false b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.If(condition).Info("should not log") } }) } // BenchmarkWithContext tests context field overhead func BenchmarkWithContext(b *testing.B) { logger := newTestLogger(lx.LevelDebug).AddContext("service", "api", "version", "1.0.0") b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Info("message with context") } }) } // BenchmarkDebugOverhead tests Debug level when set to Info (filtered) func BenchmarkDebugOverhead(b *testing.B) { logger := newTestLogger(lx.LevelInfo) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Debug("debug message filtered out") } }) } // BenchmarkPrint tests Print (LevelNone, no formatting) func BenchmarkPrint(b *testing.B) { logger := ll.New("app", ll.WithHandler(lh.NewTextHandler(io.Discard)), ).Enable() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Print("raw message") } }) } // BenchmarkSince tests timing overhead func BenchmarkSince(b *testing.B) { logger := newTestLogger(lx.LevelDebug) b.ResetTimer() for i := 0; i < b.N; i++ { sb := logger.Since() time.Sleep(time.Microsecond) // Simulate tiny work sb.Info("operation completed") } } // BenchmarkStackCapture tests stack trace capture cost func BenchmarkStackCapture(b *testing.B) { logger := newTestLogger(lx.LevelDebug) b.ResetTimer() for i := 0; i < b.N; i++ { logger.Stack("error with stack") } } // BenchmarkMeasure tests the Measure helper func BenchmarkMeasure(b *testing.B) { logger := newTestLogger(lx.LevelDebug) work := func() { time.Sleep(time.Microsecond) } b.ResetTimer() for i := 0; i < b.N; i++ { logger.Measure(work) } } // BenchmarkMultiHandler tests handler chaining overhead func BenchmarkMultiHandler(b *testing.B) { multi := lh.NewMultiHandler( lh.NewTextHandler(io.Discard), lh.NewJSONHandler(io.Discard), ) logger := ll.New("app", ll.WithHandler(multi), ll.WithLevel(lx.LevelDebug), ).Enable() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Info("multi handler message") } }) } // BenchmarkColorizedHandler tests colorized output overhead func BenchmarkColorizedHandler(b *testing.B) { logger := ll.New("app", ll.WithHandler(lh.NewColorizedHandler(io.Discard, lh.WithColorNone())), ll.WithLevel(lx.LevelDebug), ).Enable() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Info("colored message") } }) } // BenchmarkJSONHandler tests JSON serialization cost func BenchmarkJSONHandler(b *testing.B) { logger := ll.New("app", ll.WithHandler(lh.NewJSONHandler(io.Discard)), ll.WithLevel(lx.LevelDebug), ).Enable() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Fields("key", "value", "num", 42).Info("json message") } }) } // BenchmarkNamespaceEnableCheck tests namespace enablement lookup func BenchmarkNamespaceEnableCheck(b *testing.B) { root := newTestLogger(lx.LevelDebug) child := root.Namespace("child").Namespace("grandchild") b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _ = child.NamespaceEnabled("test") } }) } // BenchmarkClone tests logger cloning cost func BenchmarkClone(b *testing.B) { logger := newTestLogger(lx.LevelDebug).AddContext("key", "value") b.ResetTimer() for i := 0; i < b.N; i++ { _ = logger.Clone() } } // BenchmarkMiddleware tests middleware chain overhead func BenchmarkMiddleware(b *testing.B) { logger := newTestLogger(lx.LevelDebug) // Add a simple passthrough middleware logger.Use(ll.Middle(func(e *lx.Entry) error { return nil })) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Info("message through middleware") } }) } // BenchmarkFieldBuilderChain tests field builder chaining func BenchmarkFieldBuilderChain(b *testing.B) { logger := newTestLogger(lx.LevelDebug) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { // Chain Fields and Merge (removed Field call that was causing error) logger.Fields("a", 1).Merge("b", 2).Info("chained") } }) } // BenchmarkGlobalLogger tests package-level functions func BenchmarkGlobalLogger(b *testing.B) { ll.Handler(lh.NewTextHandler(io.Discard)) ll.Level(lx.LevelDebug) ll.Enable() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { ll.Info("global logger message") } }) } // BenchmarkSuspendCheck tests suspended logger fast-path func BenchmarkSuspendCheck(b *testing.B) { logger := newTestLogger(lx.LevelDebug).Suspend() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Info("should be suspended") } }) } // BenchmarkErrLogging tests error logging - Err() doesn't return value, so test differently func BenchmarkErrLogging(b *testing.B) { logger := newTestLogger(lx.LevelDebug) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { // Err() logs immediately and doesn't return anything for chaining // Just test the overhead of calling it with nil logger.Err(nil) } }) } // BenchmarkDbg tests debug output (source capture) func BenchmarkDbg(b *testing.B) { logger := newTestLogger(lx.LevelDebug) b.ResetTimer() for i := 0; i < b.N; i++ { logger.Dbg("debug value", i) } } // BenchmarkMark tests mark output func BenchmarkMark(b *testing.B) { logger := newTestLogger(lx.LevelDebug) b.ResetTimer() for i := 0; i < b.N; i++ { logger.Mark() } } // BenchmarkLine tests vertical spacing func BenchmarkLine(b *testing.B) { logger := newTestLogger(lx.LevelDebug) b.ResetTimer() for i := 0; i < b.N; i++ { logger.Line(1) } } // BenchmarkDump tests hex dump output func BenchmarkDump(b *testing.B) { logger := newTestLogger(lx.LevelDebug) data := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f} // "Hello" b.ResetTimer() for i := 0; i < b.N; i++ { logger.Dump(data) } } // BenchmarkOutput tests JSON output method func BenchmarkOutput(b *testing.B) { logger := newTestLogger(lx.LevelDebug) data := map[string]interface{}{"key": "value", "num": 42} b.ResetTimer() for i := 0; i < b.N; i++ { logger.Output(data) } } // BenchmarkInspect tests inspect output func BenchmarkInspect(b *testing.B) { logger := newTestLogger(lx.LevelDebug) type TestStruct struct { Name string Value int } obj := TestStruct{Name: "test", Value: 42} b.ResetTimer() for i := 0; i < b.N; i++ { logger.Inspect(obj) } } // BenchmarkFatalExits tests fatal configuration func BenchmarkFatalExits(b *testing.B) { logger := ll.New("app", ll.WithHandler(lh.NewTextHandler(io.Discard)), ll.WithLevel(lx.LevelDebug), ll.WithFatalExits(false), ).Enable() b.ResetTimer() for i := 0; i < b.N; i++ { logger.Fatal("fatal error") } } golang-github-olekukonko-ll-0.1.8/tests/buffer_test.go000066400000000000000000000275321516152337300230530ustar00rootroot00000000000000package tests import ( "bytes" "encoding/json" "errors" "fmt" "io" "os" "runtime" "strings" "sync" "sync/atomic" "testing" "time" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" ) // errorWriter is an io.Writer that always returns an error. type errorWriter struct { err error } func (w *errorWriter) Write(p []byte) (n int, err error) { return 0, w.err } // safeBuffer wraps bytes.Buffer with a mutex so tests can safely read while // background goroutines are writing. type safeBuffer struct { mu sync.Mutex b bytes.Buffer } func (s *safeBuffer) Write(p []byte) (int, error) { s.mu.Lock() defer s.mu.Unlock() return s.b.Write(p) } func (s *safeBuffer) String() string { s.mu.Lock() defer s.mu.Unlock() return s.b.String() } // blockingWriter is an io.Writer whose Write blocks until the gate channel is // closed. It is used to freeze the Buffered worker goroutine inside a flush // so that the entries channel stays full and overflow can be reliably triggered. type blockingWriter struct { gate chan struct{} // close to unblock all pending Write calls buf safeBuffer } func (w *blockingWriter) Write(p []byte) (int, error) { <-w.gate // block until released return w.buf.Write(p) } // waitUntil polls until condition is true or timeout elapses. // Helps avoid brittle sleeps while not requiring internal acks from the handler. func waitUntil(timeout time.Duration, cond func() bool) bool { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { if cond() { return true } time.Sleep(5 * time.Millisecond) } return cond() } func TestBufferedHandler(t *testing.T) { t.Run("BasicFunctionality", func(t *testing.T) { buf := &safeBuffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(2), lh.WithFlushInterval(100*time.Millisecond), lh.WithErrorOutput(io.Discard), ) _ = handler.Handle(&lx.Entry{Message: "test1"}) _ = handler.Handle(&lx.Entry{Message: "test2"}) // Give the worker some time to flush (interval-based or batch-based). ok := waitUntil(500*time.Millisecond, func() bool { out := buf.String() return strings.Contains(out, "test1") && strings.Contains(out, "test2") }) _ = handler.Close() output := buf.String() if !ok { t.Fatalf("Expected both messages in output, got: %q", output) } }) t.Run("PeriodicFlushing", func(t *testing.T) { buf := &safeBuffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(100), lh.WithFlushInterval(50*time.Millisecond), lh.WithErrorOutput(io.Discard), ) _ = handler.Handle(&lx.Entry{Message: "test"}) ok := waitUntil(500*time.Millisecond, func() bool { return strings.Contains(buf.String(), "test") }) _ = handler.Close() if !ok { t.Fatalf("Expected message to be flushed after interval, got: %q", buf.String()) } }) // OverflowHandling verifies that Handle returns an error when the internal // channel is full. // // The race: the worker goroutine drains entries from the channel into its // local batch slice one-by-one. Each read frees a channel slot, so a // naive "fill then probe" loop races with the worker and the channel may // never actually be full when the probe fires. // // Fix: use a blockingWriter whose Write blocks on a gate channel. We send // one entry and trigger a flush so the worker enters TextHandler.Write and // blocks there. While blocked it cannot read further entries from the // channel, so we can fill it to capacity and reliably trigger overflow. t.Run("OverflowHandling", func(t *testing.T) { gate := make(chan struct{}) bw := &blockingWriter{gate: gate} textHandler := lh.NewTextHandler(bw) var overflowCalled atomic.Bool handler := lh.NewBuffered(textHandler, lh.WithBatchSize(2), lh.WithMaxBuffer(2), lh.WithOverflowHandler(func(int) { overflowCalled.Store(true) }), lh.WithErrorOutput(io.Discard), ) defer handler.Close() maxBuffer := handler.Config().MaxBuffer // actual capacity (= 2 here) // Seed one entry and flush so the worker enters blockingWriter.Write // and blocks on the gate. _ = handler.Handle(&lx.Entry{Message: "seed"}) handler.Flush() // Give the worker time to reach blockingWriter.Write and block. time.Sleep(20 * time.Millisecond) // Fill the channel to capacity. The worker is frozen so no slots are // freed between iterations. for i := 0; i < maxBuffer; i++ { if err := handler.Handle(&lx.Entry{Message: fmt.Sprintf("fill%d", i)}); err != nil { close(gate) t.Fatalf("unexpected error filling slot %d: %v", i, err) } } // Channel is now full — the next Handle must overflow. err := handler.Handle(&lx.Entry{Message: "overflow"}) // Unblock the worker so deferred Close() can drain and finish cleanly. close(gate) if err == nil { t.Fatal("Expected error on overflow") } if !overflowCalled.Load() { t.Fatal("Expected overflow handler to be called") } }) t.Run("ExplicitFlush", func(t *testing.T) { buf := &safeBuffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(100), lh.WithErrorOutput(io.Discard)) defer handler.Close() _ = handler.Handle(&lx.Entry{Message: "test"}) handler.Flush() ok := waitUntil(500*time.Millisecond, func() bool { return strings.Contains(buf.String(), "test") }) if !ok { t.Fatalf("Expected message to be flushed after explicit flush, got: %q", buf.String()) } }) t.Run("ShutdownDrainsBuffer", func(t *testing.T) { buf := &safeBuffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(100), lh.WithErrorOutput(io.Discard)) _ = handler.Handle(&lx.Entry{Message: "test"}) _ = handler.Close() if !strings.Contains(buf.String(), "test") { t.Fatalf("Expected message to be flushed on shutdown, got: %q", buf.String()) } }) t.Run("ConcurrentAccess", func(t *testing.T) { buf := &safeBuffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(100), lh.WithFlushInterval(10*time.Millisecond), lh.WithMaxBuffer(2000), lh.WithErrorOutput(io.Discard), ) var wg sync.WaitGroup 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() handler.Flush() _ = handler.Close() // Stop worker; should drain/flush remaining. output := buf.String() for i := 0; i < 100; i++ { if !strings.Contains(output, fmt.Sprintf("test%d", i)) { t.Fatalf("Missing message test%d in output", i) } } }) t.Run("ErrorHandling", func(t *testing.T) { errWriter := &errorWriter{err: errors.New("write error")} textHandler := lh.NewTextHandler(errWriter) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(1), lh.WithErrorOutput(io.Discard)) defer handler.Close() // Buffered handler should accept the entry; write error occurs during flush. if err := handler.Handle(&lx.Entry{Message: "test"}); err != nil { t.Fatalf("Unexpected error on Handle: %v", err) } handler.Flush() // Inactive assertion here; this test is to ensure it doesn't race/panic. _ = waitUntil(300*time.Millisecond, func() bool { return true }) }) t.Run("Finalizer", func(t *testing.T) { buf := &safeBuffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(100), lh.WithErrorOutput(io.Discard)) _ = handler.Handle(&lx.Entry{Message: "test"}) // Ensure our test isn't relying on GC timing; call Final directly. runtime.SetFinalizer(handler, nil) handler.Final() // Calls Close() if !strings.Contains(buf.String(), "test") { t.Fatalf("Expected message to be flushed by finalizer, got: %q", buf.String()) } }) } func TestBufferedHandlerOptions(t *testing.T) { t.Run("DefaultValues", func(t *testing.T) { textHandler := lh.NewTextHandler(&safeBuffer{}) handler := lh.NewBuffered(textHandler, lh.WithErrorOutput(io.Discard)) defer handler.Close() if handler.Config().BatchSize != 100 { t.Errorf("Expected default BatchSize=100, got %d", handler.Config().BatchSize) } if handler.Config().FlushInterval != 10*time.Second { t.Errorf("Expected default FlushInterval=10s, got %v", handler.Config().FlushInterval) } if handler.Config().MaxBuffer != 1000 { t.Errorf("Expected default MaxBuffer=1000, got %d", handler.Config().MaxBuffer) } }) t.Run("CustomOptions", func(t *testing.T) { textHandler := lh.NewTextHandler(&safeBuffer{}) var called atomic.Bool handler := lh.NewBuffered(textHandler, lh.WithBatchSize(50), lh.WithFlushInterval(5*time.Second), lh.WithMaxBuffer(500), lh.WithOverflowHandler(func(int) { called.Store(true) }), lh.WithErrorOutput(io.Discard), ) defer handler.Close() if handler.Config().BatchSize != 50 { t.Errorf("Expected BatchSize=50, got %d", handler.Config().BatchSize) } if handler.Config().FlushInterval != 5*time.Second { t.Errorf("Expected FlushInterval=5s, got %v", handler.Config().FlushInterval) } if handler.Config().MaxBuffer != 500 { t.Errorf("Expected MaxBuffer=500, got %d", handler.Config().MaxBuffer) } handler.Config().OnOverflow(1) if !called.Load() { t.Error("Expected overflow handler to be called") } }) } func TestBufferedHandlerEdgeCases(t *testing.T) { t.Run("ZeroBatchSize", func(t *testing.T) { textHandler := lh.NewTextHandler(&safeBuffer{}) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(0), lh.WithErrorOutput(io.Discard)) defer handler.Close() if handler.Config().BatchSize != 1 { t.Errorf("Expected BatchSize to be adjusted to 1, got %d", handler.Config().BatchSize) } }) t.Run("NegativeFlushInterval", func(t *testing.T) { textHandler := lh.NewTextHandler(&safeBuffer{}) handler := lh.NewBuffered(textHandler, lh.WithFlushInterval(-1*time.Second)) defer handler.Close() if handler.Config().FlushInterval != 10*time.Second { t.Errorf("Expected FlushInterval to be adjusted to 10s, got %v", handler.Config().FlushInterval) } }) t.Run("SmallMaxBuffer", func(t *testing.T) { textHandler := lh.NewTextHandler(&safeBuffer{}) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(10), lh.WithMaxBuffer(5), lh.WithErrorOutput(io.Discard), ) defer handler.Close() if handler.Config().MaxBuffer < handler.Config().BatchSize { t.Errorf("Expected MaxBuffer >= BatchSize, got %d < %d", handler.Config().MaxBuffer, handler.Config().BatchSize) } }) } func TestBufferedHandlerIntegration(t *testing.T) { t.Run("WithTextHandler", func(t *testing.T) { buf := &safeBuffer{} textHandler := lh.NewTextHandler(buf) handler := lh.NewBuffered(textHandler, lh.WithBatchSize(2), lh.WithFlushInterval(50*time.Millisecond), lh.WithErrorOutput(io.Discard), ) _ = handler.Handle(&lx.Entry{Message: "message1"}) _ = handler.Handle(&lx.Entry{Message: "message2"}) ok := waitUntil(500*time.Millisecond, func() bool { out := buf.String() return strings.Contains(out, "message1") && strings.Contains(out, "message2") }) _ = handler.Close() if !ok { t.Fatalf("Expected both messages in output, got: %q", buf.String()) } }) t.Run("WithJSONHandler", func(t *testing.T) { buf := &safeBuffer{} jsonHandler := lh.NewJSONHandler(buf) handler := lh.NewBuffered(jsonHandler, lh.WithBatchSize(2)) _ = handler.Handle(&lx.Entry{Message: "message1"}) _ = handler.Handle(&lx.Entry{Message: "message2"}) handler.Flush() _ = handler.Close() // Ensure no concurrent writes during decode. dec := json.NewDecoder(strings.NewReader(buf.String())) count := 0 for dec.More() { var obj map[string]interface{} if err := dec.Decode(&obj); err != nil { t.Fatalf("Failed to decode JSON: %v", err) } count++ } if count != 2 { t.Fatalf("Expected 2 JSON objects, got %d", count) } }) } // Ensure the os import is used (kept for parity with original file). var _ = os.Stderr golang-github-olekukonko-ll-0.1.8/tests/handler_test.go000066400000000000000000000070301516152337300232060ustar00rootroot00000000000000package tests import ( "bytes" "encoding/json" "log/slog" "strings" "testing" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" ) // 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.1.8/tests/ll_ns_test.go000066400000000000000000000203441516152337300227030ustar00rootroot00000000000000// ll_ns_test.go package tests import ( "bytes" "strings" "testing" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lx" ) // 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.1.8/writer.go000066400000000000000000000021151516152337300207030ustar00rootroot00000000000000package ll import ( "bytes" "io" "strings" "github.com/olekukonko/ll/lx" ) // Writer returns an io.Writer that logs every write operation at the given level. // Useful for capturing Stdout/Stderr from external processes. func (l *Logger) Writer(level lx.LevelType) io.Writer { return &logWriter{ logger: l, level: level, } } // logWriter implements io.Writer to bridge external streams to ll.Logger type logWriter struct { logger *Logger level lx.LevelType buf bytes.Buffer // Buffer for incomplete lines } func (w *logWriter) Write(p []byte) (n int, err error) { if len(p) == 0 { return 0, nil } // Buffer handling for partial lines (streams often write byte-by-byte) w.buf.Write(p) // Process complete lines for { line, err := w.buf.ReadString('\n') if err != nil { // No newline found, buffer remains w.buf.WriteString(line) break } // Clean and log the complete line msg := strings.TrimSuffix(line, "\n") msg = strings.TrimSuffix(msg, "\r") if msg != "" { w.logger.log(w.level, lx.ClassText, msg, nil, false) } } return len(p), nil }