Initial QSfera import
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
.idea
|
||||
lab
|
||||
tmp
|
||||
#_*
|
||||
_test/
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# 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:"
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT 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.
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# 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
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
# 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.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
recursive = true
|
||||
output_file = "all.txt"
|
||||
extensions = [".go"]
|
||||
exclude_dirs = ["_examples", "_lab", "_tmp", "pkg", "lab","bin","dist","assets","oppor"]
|
||||
exclude_files = [""]
|
||||
use_gitignore = true
|
||||
detailed = true
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
package ll
|
||||
|
||||
// Conditional enables conditional logging based on a boolean condition.
|
||||
// It wraps a logger with a condition that determines whether logging operations are executed,
|
||||
// optimizing performance by skipping expensive operations (e.g., field computation, message formatting)
|
||||
// when the condition is false. The struct supports fluent chaining for adding fields and logging.
|
||||
type Conditional struct {
|
||||
logger *Logger // Associated logger instance for logging operations
|
||||
condition bool // Whether logging is allowed (true to log, false to skip)
|
||||
}
|
||||
|
||||
// If creates a conditional logger that logs only if the condition is true.
|
||||
// It returns a Conditional struct that wraps the logger, enabling conditional logging methods.
|
||||
// This method is typically called on a Logger instance to start a conditional chain.
|
||||
// Thread-safe via the underlying logger's mutex.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Info("Logged") // Output: [app] INFO: Logged
|
||||
// logger.If(false).Info("Ignored") // No output
|
||||
func (l *Logger) If(condition bool) *Conditional {
|
||||
return &Conditional{logger: l, condition: condition}
|
||||
}
|
||||
|
||||
// IfAny creates a conditional logger that logs only if at least one condition is true.
|
||||
// It evaluates a variadic list of boolean conditions, setting the condition to true if any
|
||||
// is true (logical OR). Returns a new Conditional with the result. Thread-safe via the
|
||||
// underlying logger.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.IfAny(false, true).Info("Logged") // Output: [app] INFO: Logged
|
||||
// logger.IfAny(false, false).Info("Ignored") // No output
|
||||
func (cl *Conditional) IfAny(conditions ...bool) *Conditional {
|
||||
result := false
|
||||
// Check each condition; set result to true if any is true
|
||||
for _, cond := range conditions {
|
||||
if cond {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return &Conditional{logger: cl.logger, condition: result}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return &Conditional{logger: cl.logger, condition: cl.condition && true}
|
||||
}
|
||||
}
|
||||
return &Conditional{logger: cl.logger, condition: false}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return &Conditional{logger: cl.logger, condition: false}
|
||||
}
|
||||
}
|
||||
return &Conditional{logger: cl.logger, condition: cl.condition && len(errs) > 0}
|
||||
}
|
||||
|
||||
// IfOne creates a conditional logger that logs only if all conditions are true.
|
||||
// It evaluates a variadic list of boolean conditions, setting the condition to true only if
|
||||
// all are true (logical AND). Returns a new Conditional with the result. Thread-safe via the
|
||||
// underlying logger.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.IfOne(true, true).Info("Logged") // Output: [app] INFO: Logged
|
||||
// logger.IfOne(true, false).Info("Ignored") // No output
|
||||
func (cl *Conditional) IfOne(conditions ...bool) *Conditional {
|
||||
result := true
|
||||
// Check each condition; set result to false if any is false
|
||||
for _, cond := range conditions {
|
||||
if !cond {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return &Conditional{logger: cl.logger, condition: result}
|
||||
}
|
||||
|
||||
// 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
|
||||
if !cl.condition {
|
||||
return &FieldBuilder{logger: cl.logger, 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
|
||||
if !cl.condition {
|
||||
return &FieldBuilder{logger: cl.logger, 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...)
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package 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 != ""
|
||||
}
|
||||
}
|
||||
}
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
// field.go
|
||||
package ll
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/cat"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Logger creates a new logger with the builder's fields embedded in its context.
|
||||
// It clones the parent logger and copies the builder's fields into the new logger's context,
|
||||
// enabling persistent field inclusion in subsequent logs. This method supports fluent chaining
|
||||
// after Fields or Field calls.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// newLogger := logger.Fields("user", "alice").Logger()
|
||||
// newLogger.Info("Action") // Output: [app] INFO: Action [user=alice]
|
||||
func (fb *FieldBuilder) Logger() *Logger {
|
||||
// Clone the parent logger to preserve its configuration
|
||||
newLogger := fb.logger.Clone()
|
||||
// Copy builder's fields into the new logger's context
|
||||
newLogger.context = make(lx.Fields, len(fb.fields))
|
||||
copy(newLogger.context, 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,
|
||||
// returning early if fields are nil. This method is used for informational messages.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Info("Action", "started") // Output: [app] INFO: Action started [user=alice]
|
||||
func (fb *FieldBuilder) Info(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Log at Info level with the builder's fields, no stack trace
|
||||
fb.logger.log(lx.LevelInfo, lx.ClassText, cat.Space(args...), fb.fields, false)
|
||||
}
|
||||
|
||||
// Infof logs a message at Info level with the builder's fields.
|
||||
// It formats the message using the provided format string and arguments, then delegates
|
||||
// to the logger's internal log method. If fields are nil, it returns early to avoid logging.
|
||||
// This method is part of the fluent API, typically called after adding fields.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Infof("Action %s", "started") // Output: [app] INFO: Action started [user=alice]
|
||||
func (fb *FieldBuilder) Infof(format string, args ...any) {
|
||||
// Skip logging if fields are nil to prevent invalid log entries
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message using the provided arguments
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
// Log at Info level with the builder's fields, no stack trace
|
||||
fb.logger.log(lx.LevelInfo, lx.ClassText, msg, fb.fields, false)
|
||||
}
|
||||
|
||||
// Debug logs a message at Debug level with the builder's fields.
|
||||
// It concatenates the arguments with spaces and delegates to the logger's log method,
|
||||
// returning early if fields are nil. This method is used for debugging information.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Debug("Debugging", "mode") // Output: [app] DEBUG: Debugging mode [user=alice]
|
||||
func (fb *FieldBuilder) Debug(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Log at Debug level with the builder's fields, no stack trace
|
||||
fb.logger.log(lx.LevelDebug, lx.ClassText, cat.Space(args...), fb.fields, false)
|
||||
}
|
||||
|
||||
// Debugf logs a message at Debug level with the builder's fields.
|
||||
// It formats the message and delegates to the logger's log method, returning early if
|
||||
// fields are nil. This method is used for debugging information that may be disabled in
|
||||
// production environments.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Debugf("Debug %s", "mode") // Output: [app] DEBUG: Debug mode [user=alice]
|
||||
func (fb *FieldBuilder) Debugf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
// Log at Debug level with the builder's fields, no stack trace
|
||||
fb.logger.log(lx.LevelDebug, lx.ClassText, msg, fb.fields, false)
|
||||
}
|
||||
|
||||
// Warn logs a message at Warn level with the builder's fields.
|
||||
// It concatenates the arguments with spaces and delegates to the logger's log method,
|
||||
// returning early if fields are nil. This method is used for warning conditions.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Warn("Warning", "issued") // Output: [app] WARN: Warning issued [user=alice]
|
||||
func (fb *FieldBuilder) Warn(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Log at Warn level with the builder's fields, no stack trace
|
||||
fb.logger.log(lx.LevelWarn, lx.ClassText, cat.Space(args...), fb.fields, false)
|
||||
}
|
||||
|
||||
// Warnf logs a message at Warn level with the builder's fields.
|
||||
// It formats the message and delegates to the logger's log method, returning early if
|
||||
// fields are nil. This method is used for warning conditions that do not halt execution.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued [user=alice]
|
||||
func (fb *FieldBuilder) Warnf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
// Log at Warn level with the builder's fields, no stack trace
|
||||
fb.logger.log(lx.LevelWarn, lx.ClassText, msg, fb.fields, false)
|
||||
}
|
||||
|
||||
// Error logs a message at Error level with the builder's fields.
|
||||
// It concatenates the arguments with spaces and delegates to the logger's log method,
|
||||
// returning early if fields are nil. This method is used for error conditions.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Error("Error", "occurred") // Output: [app] ERROR: Error occurred [user=alice]
|
||||
func (fb *FieldBuilder) Error(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Log at Error level with the builder's fields, no stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, cat.Space(args...), fb.fields, false)
|
||||
}
|
||||
|
||||
// Errorf logs a message at Error level with the builder's fields.
|
||||
// It formats the message and delegates to the logger's log method, returning early if
|
||||
// fields are nil. This method is used for error conditions that may require attention.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred [user=alice]
|
||||
func (fb *FieldBuilder) Errorf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
// Log at Error level with the builder's fields, no stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, false)
|
||||
}
|
||||
|
||||
// Stack logs a message at Error level with a stack trace and the builder's fields.
|
||||
// It concatenates the arguments with spaces and delegates to the logger's log method,
|
||||
// returning early if fields are nil. This method is useful for debugging critical errors.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Stack("Critical", "error") // Output: [app] ERROR: Critical error [user=alice stack=...]
|
||||
func (fb *FieldBuilder) Stack(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Log at Error level with the builder's fields and a stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, cat.Space(args...), fb.fields, true)
|
||||
}
|
||||
|
||||
// Stackf logs a message at Error level with a stack trace and the builder's fields.
|
||||
// It formats the message and delegates to the logger's log method, returning early if
|
||||
// fields are nil. This method is useful for debugging critical errors.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [user=alice stack=...]
|
||||
func (fb *FieldBuilder) Stackf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
// Log at Error level with the builder's fields and a stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, true)
|
||||
}
|
||||
|
||||
// Fatal logs a message at Error level with a stack trace and the builder's fields, then exits.
|
||||
// It constructs the message from variadic arguments, logs it with a stack trace, and terminates
|
||||
// the program with exit code 1. Returns early if fields are nil. This method is used for
|
||||
// unrecoverable errors.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Fatal("Fatal", "error") // Output: [app] ERROR: Fatal error [user=alice stack=...], then exits
|
||||
func (fb *FieldBuilder) Fatal(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Build the message by concatenating arguments with spaces
|
||||
var builder strings.Builder
|
||||
for i, arg := range args {
|
||||
if i > 0 {
|
||||
builder.WriteString(lx.Space)
|
||||
}
|
||||
builder.WriteString(fmt.Sprint(arg))
|
||||
}
|
||||
// Log at Error level with the builder's fields and a stack trace
|
||||
fb.logger.log(lx.LevelFatal, lx.ClassText, builder.String(), fb.fields, fb.logger.fatalStack)
|
||||
|
||||
// Exit the program with status code 1
|
||||
if fb.logger.fatalExits {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Fatalf logs a formatted message at Error level with a stack trace and the builder's fields,
|
||||
// then exits. It delegates to Fatal and returns early if fields are nil. This method is used
|
||||
// for unrecoverable errors.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [user=alice stack=...], then exits
|
||||
func (fb *FieldBuilder) Fatalf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message and pass to Fatal
|
||||
fb.Fatal(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Panic logs a message at Error level with a stack trace and the builder's fields, then panics.
|
||||
// It constructs the message from variadic arguments, logs it with a stack trace, and triggers
|
||||
// a panic with the message. Returns early if fields are nil. This method is used for critical
|
||||
// errors that require immediate program termination with a panic.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Panic("Panic", "error") // Output: [app] ERROR: Panic error [user=alice stack=...], then panics
|
||||
func (fb *FieldBuilder) Panic(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Build the message by concatenating arguments with spaces
|
||||
var builder strings.Builder
|
||||
for i, arg := range args {
|
||||
if i > 0 {
|
||||
builder.WriteString(lx.Space)
|
||||
}
|
||||
builder.WriteString(fmt.Sprint(arg))
|
||||
}
|
||||
msg := builder.String()
|
||||
// Log at Error level with the builder's fields and a stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, true)
|
||||
// Trigger a panic with the formatted message
|
||||
panic(msg)
|
||||
}
|
||||
|
||||
// Panicf logs a formatted message at Error level with a stack trace and the builder's fields,
|
||||
// then panics. It delegates to Panic and returns early if fields are nil. This method is used
|
||||
// for critical errors that require immediate program termination with a panic.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Panicf("Panic %s", "error") // Output: [app] ERROR: Panic error [user=alice stack=...], then panics
|
||||
func (fb *FieldBuilder) Panicf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message and pass to Panic
|
||||
fb.Panic(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Err adds one or more errors to the FieldBuilder as a field and logs them.
|
||||
// It stores non-nil errors in the "error" field: a single error if only one is non-nil,
|
||||
// or a slice of errors if multiple are non-nil. It logs the concatenated string representations
|
||||
// of non-nil errors (e.g., "failed 1; failed 2") at the Error level. Returns the FieldBuilder
|
||||
// for chaining, allowing further field additions or logging. Thread-safe via the logger's mutex.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// err1 := errors.New("failed 1")
|
||||
// err2 := errors.New("failed 2")
|
||||
// logger.Fields("k", "v").Err(err1, err2).Info("Error occurred")
|
||||
// // Output: [app] ERROR: failed 1; failed 2
|
||||
// // [app] INFO: Error occurred [error=[failed 1 failed 2] k=v]
|
||||
func (fb *FieldBuilder) Err(errs ...error) *FieldBuilder {
|
||||
// Initialize fields slice if nil
|
||||
if fb.fields == nil {
|
||||
fb.fields = 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++
|
||||
}
|
||||
}
|
||||
|
||||
// Set error field and log if there are non-nil errors
|
||||
if count > 0 {
|
||||
if count == 1 {
|
||||
// Store single error directly
|
||||
fb.fields = append(fb.fields, lx.Field{Key: "error", Value: nonNilErrors[0]})
|
||||
} else {
|
||||
// Store slice of errors
|
||||
fb.fields = append(fb.fields, lx.Field{Key: "error", Value: nonNilErrors})
|
||||
}
|
||||
// Log concatenated error messages at Error level
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, builder.String(), nil, false)
|
||||
}
|
||||
|
||||
// Return FieldBuilder for chaining
|
||||
return fb
|
||||
}
|
||||
|
||||
// Merge adds additional key-value pairs to the FieldBuilder.
|
||||
// It processes variadic arguments as key-value pairs, expecting string keys. Non-string keys
|
||||
// or uneven pairs generate an "error" field with a descriptive message. Returns the FieldBuilder
|
||||
// for chaining to allow further field additions or logging.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("k1", "v1").Merge("k2", "v2").Info("Action") // Output: [app] INFO: Action [k1=v1 k2=v2]
|
||||
func (fb *FieldBuilder) Merge(pairs ...any) *FieldBuilder {
|
||||
// Initialize fields slice if nil
|
||||
if fb.fields == nil {
|
||||
fb.fields = make(lx.Fields, 0, len(pairs)/2)
|
||||
}
|
||||
|
||||
// Process pairs as key-value, advancing by 2
|
||||
for i := 0; i < len(pairs)-1; i += 2 {
|
||||
// Ensure the key is a string
|
||||
if key, ok := pairs[i].(string); ok {
|
||||
fb.fields = append(fb.fields, lx.Field{Key: key, Value: pairs[i+1]})
|
||||
} else {
|
||||
// Log an error field for non-string keys
|
||||
fb.fields = append(fb.fields, lx.Field{
|
||||
Key: "error",
|
||||
Value: fmt.Errorf("non-string key in Merge: %v", pairs[i]),
|
||||
})
|
||||
}
|
||||
}
|
||||
// Check for uneven pairs (missing value)
|
||||
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
|
||||
}
|
||||
+707
@@ -0,0 +1,707 @@
|
||||
package 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.LevelWarn)
|
||||
// ll.Info("Ignored") // No output
|
||||
// ll.Warn("Logged") // Output: [] WARN: Logged
|
||||
func Level(level lx.LevelType) *Logger {
|
||||
return defaultLogger.Level(level)
|
||||
}
|
||||
|
||||
// Style sets the namespace style for the default logger.
|
||||
// It controls how namespace paths are formatted in logs (FlatPath: [parent/child],
|
||||
// NestedPath: [parent]→[child]). Returns the default logger for method chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Style(lx.NestedPath)
|
||||
// ll.Info("Test") // Output: []: INFO: Test
|
||||
func Style(style lx.StyleType) *Logger {
|
||||
return defaultLogger.Style(style)
|
||||
}
|
||||
|
||||
// NamespaceEnable enables logging for a namespace and its children using the default logger.
|
||||
// It activates logging for the specified namespace path (e.g., "app/db") and all its
|
||||
// descendants. Returns the default logger for method chaining. Thread-safe via the Logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// ll.NamespaceEnable("app/db")
|
||||
// ll.Clone().Namespace("db").Info("Query") // Output: [app/db] INFO: Query
|
||||
func NamespaceEnable(path string) *Logger {
|
||||
return defaultLogger.NamespaceEnable(path)
|
||||
}
|
||||
|
||||
// NamespaceDisable disables logging for a namespace and its children using the default logger.
|
||||
// It suppresses logging for the specified namespace path and all its descendants. Returns
|
||||
// the default logger for method chaining. Thread-safe via the Logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// ll.NamespaceDisable("app/db")
|
||||
// ll.Clone().Namespace("db").Info("Query") // No output
|
||||
func NamespaceDisable(path string) *Logger {
|
||||
return defaultLogger.NamespaceDisable(path)
|
||||
}
|
||||
|
||||
// Namespace creates a child logger with a sub-namespace appended to the current path.
|
||||
// The child inherits the default logger’s configuration but has an independent context.
|
||||
// Thread-safe with read lock. Returns the new logger for further configuration or logging.
|
||||
// Example:
|
||||
//
|
||||
// logger := ll.Namespace("app")
|
||||
// logger.Info("Started") // Output: [app] INFO: Started
|
||||
func Namespace(name string) *Logger {
|
||||
return defaultLogger.Namespace(name)
|
||||
}
|
||||
|
||||
// Info logs a message at Info level with variadic arguments using the default logger.
|
||||
// It concatenates the arguments with spaces and delegates to defaultLogger’s Info method.
|
||||
// Thread-safe via the Logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Info("Service", "started") // Output: [] INFO: Service started
|
||||
func Info(args ...any) {
|
||||
defaultLogger.Info(args...)
|
||||
}
|
||||
|
||||
// Infof logs a message at Info level with a format string using the default logger.
|
||||
// It formats the message using the provided format string and arguments, then delegates to
|
||||
// defaultLogger’s Infof method. Thread-safe via the Logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Infof("Service %s", "started") // Output: [] INFO: Service started
|
||||
func Infof(format string, args ...any) {
|
||||
defaultLogger.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs a message at Debug level with variadic arguments using the default logger.
|
||||
// It concatenates the arguments with spaces and delegates to defaultLogger’s Debug method.
|
||||
// Used for debugging information, typically disabled in production. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Level(lx.LevelDebug)
|
||||
// ll.Debug("Debugging", "mode") // Output: [] DEBUG: Debugging mode
|
||||
func Debug(args ...any) {
|
||||
defaultLogger.Debug(args...)
|
||||
}
|
||||
|
||||
// Debugf logs a message at Debug level with a format string using the default logger.
|
||||
// It formats the message and delegates to defaultLogger’s Debugf method. Used for debugging
|
||||
// information, typically disabled in production. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Level(lx.LevelDebug)
|
||||
// ll.Debugf("Debug %s", "mode") // Output: [] DEBUG: Debug mode
|
||||
func Debugf(format string, args ...any) {
|
||||
defaultLogger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Warn logs a message at Warn level with variadic arguments using the default logger.
|
||||
// It concatenates the arguments with spaces and delegates to defaultLogger’s Warn method.
|
||||
// Used for warning conditions that do not halt execution. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Warn("Low", "memory") // Output: [] WARN: Low memory
|
||||
func Warn(args ...any) {
|
||||
defaultLogger.Warn(args...)
|
||||
}
|
||||
|
||||
// Warnf logs a message at Warn level with a format string using the default logger.
|
||||
// It formats the message and delegates to defaultLogger’s Warnf method. Used for warning
|
||||
// conditions that do not halt execution. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Warnf("Low %s", "memory") // Output: [] WARN: Low memory
|
||||
func Warnf(format string, args ...any) {
|
||||
defaultLogger.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Error logs a message at Error level with variadic arguments using the default logger.
|
||||
// It concatenates the arguments with spaces and delegates to defaultLogger’s Error method.
|
||||
// Used for error conditions requiring attention. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Error("Database", "failure") // Output: [] ERROR: Database failure
|
||||
func Error(args ...any) {
|
||||
defaultLogger.Error(args...)
|
||||
}
|
||||
|
||||
// Errorf logs a message at Error level with a format string using the default logger.
|
||||
// It formats the message and delegates to defaultLogger’s Errorf method. Used for error
|
||||
// conditions requiring attention. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Errorf("Database %s", "failure") // Output: [] ERROR: Database failure
|
||||
func Errorf(format string, args ...any) {
|
||||
defaultLogger.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Stack logs a message at Error level with a stack trace and variadic arguments using the default logger.
|
||||
// It concatenates the arguments with spaces and delegates to defaultLogger’s Stack method.
|
||||
// Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Stack("Critical", "error") // Output: [] ERROR: Critical error [stack=...]
|
||||
func Stack(args ...any) {
|
||||
defaultLogger.Stack(args...)
|
||||
}
|
||||
|
||||
// Stackf logs a message at Error level with a stack trace and a format string using the default logger.
|
||||
// It formats the message and delegates to defaultLogger’s Stackf method. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Stackf("Critical %s", "error") // Output: [] ERROR: Critical error [stack=...]
|
||||
func Stackf(format string, args ...any) {
|
||||
defaultLogger.Stackf(format, args...)
|
||||
}
|
||||
|
||||
// Fatal logs a message at Error level with a stack trace and variadic arguments using the default logger,
|
||||
// then exits. It concatenates the arguments with spaces, logs with a stack trace, and terminates
|
||||
// with exit code 1. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Fatal("Fatal", "error") // Output: [] ERROR: Fatal error [stack=...], then exits
|
||||
func Fatal(args ...any) {
|
||||
defaultLogger.Fatal(args...)
|
||||
}
|
||||
|
||||
// Fatalf logs a formatted message at Error level with a stack trace using the default logger,
|
||||
// then exits. It formats the message, logs with a stack trace, and terminates with exit code 1.
|
||||
// Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Fatalf("Fatal %s", "error") // Output: [] ERROR: Fatal error [stack=...], then exits
|
||||
func Fatalf(format string, args ...any) {
|
||||
defaultLogger.Fatalf(format, args...)
|
||||
}
|
||||
|
||||
// Panic logs a message at Error level with a stack trace and variadic arguments using the default logger,
|
||||
// then panics. It concatenates the arguments with spaces, logs with a stack trace, and triggers a panic.
|
||||
// Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Panic("Panic", "error") // Output: [] ERROR: Panic error [stack=...], then panics
|
||||
func Panic(args ...any) {
|
||||
defaultLogger.Panic(args...)
|
||||
}
|
||||
|
||||
// Panicf logs a formatted message at Error level with a stack trace using the default logger,
|
||||
// then panics. It formats the message, logs with a stack trace, and triggers a panic. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Panicf("Panic %s", "error") // Output: [] ERROR: Panic error [stack=...], then panics
|
||||
func Panicf(format string, args ...any) {
|
||||
defaultLogger.Panicf(format, args...)
|
||||
}
|
||||
|
||||
// If creates a conditional logger that logs only if the condition is true using the default logger.
|
||||
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") // No output
|
||||
// mw.Remove()
|
||||
// ll.Info("Logged") // Output: [] INFO: Logged
|
||||
func Use(fn lx.Handler) *Middleware {
|
||||
return defaultLogger.Use(fn)
|
||||
}
|
||||
|
||||
// Remove removes middleware by the reference returned from Use for the default logger.
|
||||
// It delegates to the Middleware’s Remove method for thread-safe removal.
|
||||
// Example:
|
||||
//
|
||||
// mw := ll.Use(someMiddleware)
|
||||
// ll.Remove(mw) // Removes middleware
|
||||
func Remove(m *Middleware) {
|
||||
defaultLogger.Remove(m)
|
||||
}
|
||||
|
||||
// Clear removes all middleware functions from the default logger.
|
||||
// It resets the middleware chain to empty, ensuring no middleware is applied.
|
||||
// Thread-safe with write lock. Returns the default logger for chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Use(someMiddleware)
|
||||
// ll.Clear()
|
||||
// ll.Info("No middleware") // Output: [] INFO: No middleware
|
||||
func Clear() *Logger {
|
||||
return defaultLogger.Clear()
|
||||
}
|
||||
|
||||
// CanLog checks if a log at the given level would be emitted by the default logger.
|
||||
// It considers enablement, log level, namespaces, sampling, and rate limits.
|
||||
// Thread-safe via the Logger’s shouldLog method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Level(lx.LevelWarn)
|
||||
// canLog := ll.CanLog(lx.LevelInfo) // false
|
||||
func CanLog(level lx.LevelType) bool {
|
||||
return defaultLogger.CanLog(level)
|
||||
}
|
||||
|
||||
// NamespaceEnabled checks if a namespace is enabled in the default logger.
|
||||
// It evaluates the namespace hierarchy, considering parent namespaces, and caches the result
|
||||
// for performance. Thread-safe with read lock.
|
||||
// Example:
|
||||
//
|
||||
// ll.NamespaceDisable("app/db")
|
||||
// enabled := ll.NamespaceEnabled("app/db") // false
|
||||
func NamespaceEnabled(path string) bool {
|
||||
return defaultLogger.NamespaceEnabled(path)
|
||||
}
|
||||
|
||||
// Print logs a message at Info level without format specifiers using the default logger.
|
||||
// It concatenates variadic arguments with spaces, minimizing allocations, and delegates
|
||||
// to defaultLogger’s Print method. Thread-safe via the Logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Print("message", "value") // Output: [] INFO: message value
|
||||
func Print(args ...any) {
|
||||
defaultLogger.Print(args...)
|
||||
}
|
||||
|
||||
// Println logs a message at Info level without format specifiers, minimizing allocations
|
||||
// by concatenating arguments with spaces. It is thread-safe via the log method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Println("message", "value") // Output: [] INFO: message value [New Line]
|
||||
func Println(args ...any) {
|
||||
defaultLogger.Println(args...)
|
||||
}
|
||||
|
||||
// Printf logs a message at Info level with a format string using the default logger.
|
||||
// It formats the message and delegates to defaultLogger’s Printf method. Thread-safe via
|
||||
// the Logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Printf("Message %s", "value") // Output: [] INFO: Message value
|
||||
func Printf(format string, args ...any) {
|
||||
defaultLogger.Printf(format, args...)
|
||||
}
|
||||
|
||||
// Len returns the total number of log entries sent to the handler by the default logger.
|
||||
// It provides thread-safe access to the entries counter using atomic operations.
|
||||
// Example:
|
||||
//
|
||||
// ll.Info("Test")
|
||||
// count := ll.Len() // Returns 1
|
||||
func Len() int64 {
|
||||
return defaultLogger.Len()
|
||||
}
|
||||
|
||||
// Measure is a benchmarking helper that measures and returns the duration of a function’s execution.
|
||||
// It logs the duration at Info level with a "duration" field using defaultLogger. The function
|
||||
// is executed once, and the elapsed time is returned. Thread-safe via the Logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// duration := ll.Measure(func() { time.Sleep(time.Millisecond) })
|
||||
// // Output: [] INFO: function executed [duration=~1ms]
|
||||
func Measure(fns ...func()) time.Duration {
|
||||
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") // No output
|
||||
// ll.Start()
|
||||
// ll.Info("Logged") // Output: [] INFO: Logged
|
||||
func Start() {
|
||||
atomic.StoreInt32(&systemActive, 1)
|
||||
}
|
||||
|
||||
// Shutdown deactivates the global logging system.
|
||||
// All logging operations are skipped, regardless of individual logger or namespace configurations,
|
||||
// until Start() is called again. Thread-safe via atomic operations.
|
||||
// Example:
|
||||
//
|
||||
// ll.Shutdown()
|
||||
// ll.Info("Ignored") // No output
|
||||
func Shutdown() {
|
||||
atomic.StoreInt32(&systemActive, 0)
|
||||
}
|
||||
|
||||
// Active returns true if the global logging system is currently active.
|
||||
// Thread-safe via atomic operations.
|
||||
// Example:
|
||||
//
|
||||
// if ll.Active() {
|
||||
// ll.Info("System active") // Output: [] INFO: System active
|
||||
// }
|
||||
func Active() bool {
|
||||
return atomic.LoadInt32(&systemActive) == 1
|
||||
}
|
||||
|
||||
// Enable activates logging for the default logger.
|
||||
// It allows logs to be emitted if other conditions (level, namespace) are met.
|
||||
// Thread-safe with write lock. Returns the default logger for method chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Disable()
|
||||
// ll.Info("Ignored") // No output
|
||||
// ll.Enable()
|
||||
// ll.Info("Logged") // Output: [] INFO: Logged
|
||||
func Enable() *Logger {
|
||||
return defaultLogger.Enable()
|
||||
}
|
||||
|
||||
// Disable deactivates logging for the default logger.
|
||||
// It suppresses all logs, regardless of level or namespace. Thread-safe with write lock.
|
||||
// Returns the default logger for method chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Disable()
|
||||
// ll.Info("Ignored") // No output
|
||||
func Disable() *Logger {
|
||||
return defaultLogger.Disable()
|
||||
}
|
||||
|
||||
// Dbg logs debug information including the source file, line number, and expression value
|
||||
// using the default logger. It captures the calling line of code and displays both the
|
||||
// expression and its value. Useful for debugging without temporary print statements.
|
||||
// Example:
|
||||
//
|
||||
// x := 42
|
||||
// ll.Dbg(x) // Output: [file.go:123] x = 42
|
||||
func Dbg(any ...interface{}) {
|
||||
defaultLogger.dbg(2, any...)
|
||||
}
|
||||
|
||||
// Dump displays a hex and ASCII representation of a value’s binary form using the default logger.
|
||||
// It serializes the value using gob encoding or direct conversion and shows a hex/ASCII dump.
|
||||
// Useful for inspecting binary data structures.
|
||||
// Example:
|
||||
//
|
||||
// ll.Dump([]byte{0x41, 0x42}) // Outputs hex/ASCII dump
|
||||
func Dump(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)
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
package 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
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package 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)
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// Buffering holds configuration for the Buffered handler.
|
||||
type Buffering struct {
|
||||
BatchSize int // Flush when this many entries are buffered (default: 100)
|
||||
FlushInterval time.Duration // Maximum time between flushes (default: 10s)
|
||||
MaxBuffer int // Maximum buffer size before applying backpressure (default: 1000)
|
||||
OnOverflow func(int) // Called when buffer reaches MaxBuffer (default: logs warning)
|
||||
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.
|
||||
// It specifies the number of log entries to buffer before flushing to the underlying handler.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithBatchSize(50)) // Flush every 50 entries
|
||||
func WithBatchSize(size int) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.BatchSize = size
|
||||
}
|
||||
}
|
||||
|
||||
// WithFlushInterval sets the maximum time between flushes.
|
||||
// It defines the interval at which buffered entries are flushed, even if the batch size is not reached.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithFlushInterval(5*time.Second)) // Flush every 5 seconds
|
||||
func WithFlushInterval(d time.Duration) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.FlushInterval = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxBuffer sets the maximum buffer size before backpressure.
|
||||
// It limits the number of entries that can be queued in the channel, triggering overflow handling if exceeded.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithMaxBuffer(500)) // Allow up to 500 buffered entries
|
||||
func WithMaxBuffer(size int) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.MaxBuffer = size
|
||||
}
|
||||
}
|
||||
|
||||
// WithOverflowHandler sets the overflow callback.
|
||||
// It specifies a function to call when the buffer reaches MaxBuffer, typically for logging or metrics.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithOverflowHandler(func(n int) { fmt.Printf("Overflow: %d entries\n", n) }))
|
||||
func WithOverflowHandler(fn func(int)) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.OnOverflow = fn
|
||||
}
|
||||
}
|
||||
|
||||
// WithErrorOutput sets the destination for internal errors (e.g., downstream handler failures).
|
||||
// Defaults to os.Stderr if not set.
|
||||
// Example:
|
||||
//
|
||||
// // Redirect internal errors to a file or discard them
|
||||
// handler := NewBuffered(textHandler, WithErrorOutput(os.Stdout))
|
||||
func WithErrorOutput(w io.Writer) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.ErrorOutput = w
|
||||
}
|
||||
}
|
||||
|
||||
// Buffered wraps any Handler to provide buffering capabilities.
|
||||
// It buffers log entries in a channel and flushes them based on batch size, time interval, or explicit flush.
|
||||
// The generic type H ensures compatibility with any lx.Handler implementation.
|
||||
// Thread-safe via channels and sync primitives.
|
||||
type Buffered[H lx.Handler] struct {
|
||||
handler H // Underlying handler to process log entries
|
||||
config *Buffering // Configuration for batching and flushing
|
||||
entries chan *lx.Entry // Channel for buffering log entries
|
||||
flushSignal chan struct{} // Channel to trigger explicit flushes
|
||||
shutdown chan struct{} // Channel to signal worker shutdown
|
||||
shutdownOnce sync.Once // Ensures Close is called only once
|
||||
wg sync.WaitGroup // Waits for worker goroutine to finish
|
||||
}
|
||||
|
||||
// NewBuffered creates a new buffered handler that wraps another handler.
|
||||
// It initializes the handler with default or provided configuration options and starts a worker goroutine.
|
||||
// Thread-safe via channel operations and finalizer for cleanup.
|
||||
// Example:
|
||||
//
|
||||
// textHandler := lh.NewTextHandler(os.Stdout)
|
||||
// buffered := NewBuffered(textHandler, WithBatchSize(50))
|
||||
func NewBuffered[H lx.Handler](handler H, opts ...BufferingOpt) *Buffered[H] {
|
||||
// Initialize default configuration
|
||||
config := &Buffering{
|
||||
BatchSize: 100, // Default: flush every 100 entries
|
||||
FlushInterval: 10 * time.Second, // Default: flush every 10 seconds
|
||||
MaxBuffer: 1000, // Default: max 1000 entries in buffer
|
||||
ErrorOutput: os.Stderr, // Default: report errors to stderr
|
||||
OnOverflow: func(count int) { // Default: log overflow to io.Discard (silent by default for overflow)
|
||||
fmt.Fprintf(io.Discard, "log buffer overflow: %d entries\n", count)
|
||||
},
|
||||
}
|
||||
|
||||
// Apply provided options
|
||||
for _, opt := range opts {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
// Ensure sane configuration values
|
||||
if config.BatchSize < 1 {
|
||||
config.BatchSize = 1 // Minimum batch size is 1
|
||||
}
|
||||
if config.MaxBuffer < config.BatchSize {
|
||||
config.MaxBuffer = config.BatchSize * 10 // Ensure buffer is at least 10x batch size
|
||||
}
|
||||
if config.FlushInterval <= 0 {
|
||||
config.FlushInterval = 10 * time.Second // Minimum flush interval is 10s
|
||||
}
|
||||
if config.ErrorOutput == nil {
|
||||
config.ErrorOutput = os.Stderr
|
||||
}
|
||||
|
||||
// Initialize Buffered handler
|
||||
b := &Buffered[H]{
|
||||
handler: handler, // Set underlying handler
|
||||
config: config, // Set configuration
|
||||
entries: make(chan *lx.Entry, config.MaxBuffer), // Create buffered channel
|
||||
flushSignal: make(chan struct{}, 1), // Create single-slot flush signal channel
|
||||
shutdown: make(chan struct{}), // Create shutdown signal channel
|
||||
}
|
||||
|
||||
// Start worker goroutine
|
||||
b.wg.Add(1)
|
||||
go b.worker()
|
||||
|
||||
// Set finalizer for cleanup during garbage collection
|
||||
runtime.SetFinalizer(b, (*Buffered[H]).Final)
|
||||
return b
|
||||
}
|
||||
|
||||
// Handle implements the lx.Handler interface.
|
||||
// It buffers log entries in the entries channel or triggers a flush on overflow.
|
||||
// Returns an error if the buffer is full and flush cannot be triggered.
|
||||
// Thread-safe via non-blocking channel operations.
|
||||
// Example:
|
||||
//
|
||||
// buffered.Handle(&lx.Entry{Message: "test"}) // Buffers entry or triggers flush
|
||||
func (b *Buffered[H]) Handle(e *lx.Entry) error {
|
||||
select {
|
||||
case b.entries <- e: // Buffer entry if channel has space
|
||||
return nil
|
||||
default: // Handle buffer overflow
|
||||
if b.config.OnOverflow != nil {
|
||||
b.config.OnOverflow(len(b.entries)) // Call overflow handler
|
||||
}
|
||||
select {
|
||||
case b.flushSignal <- struct{}{}: // Trigger flush if possible
|
||||
return fmt.Errorf("log buffer overflow, triggering flush")
|
||||
default: // Flush already in progress
|
||||
return fmt.Errorf("log buffer overflow and flush already in progress")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush triggers an immediate flush of buffered entries.
|
||||
// It sends a signal to the worker to process all buffered entries.
|
||||
// If a flush is already pending, it waits briefly and may exit without flushing.
|
||||
// Thread-safe via non-blocking channel operations.
|
||||
// Example:
|
||||
//
|
||||
// buffered.Flush() // Flushes all buffered entries
|
||||
func (b *Buffered[H]) Flush() {
|
||||
select {
|
||||
case b.flushSignal <- struct{}{}: // Signal worker to flush
|
||||
case <-time.After(100 * time.Millisecond): // Timeout if flush is pending
|
||||
// Flush already pending
|
||||
}
|
||||
}
|
||||
|
||||
// Close flushes any remaining entries and stops the worker.
|
||||
// It ensures shutdown is performed only once and waits for the worker to finish.
|
||||
// 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) // Signal worker to shut down
|
||||
b.wg.Wait() // Wait for worker to finish
|
||||
runtime.SetFinalizer(b, nil) // Remove finalizer
|
||||
|
||||
// Check if underlying handler has a Close method and call it
|
||||
if closer, ok := any(b.handler).(interface{ Close() error }); ok {
|
||||
closeErr = closer.Close()
|
||||
}
|
||||
})
|
||||
return closeErr
|
||||
}
|
||||
|
||||
// Final ensures remaining entries are flushed during garbage collection.
|
||||
// It calls Close to flush entries and stop the worker.
|
||||
// Used as a runtime finalizer to prevent log loss.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// runtime.SetFinalizer(buffered, (*Buffered[H]).Final)
|
||||
func (b *Buffered[H]) Final() {
|
||||
b.Close()
|
||||
}
|
||||
|
||||
// Config returns the current configuration of the Buffered handler.
|
||||
// It provides access to BatchSize, FlushInterval, MaxBuffer, and OnOverflow settings.
|
||||
// Example:
|
||||
//
|
||||
// config := buffered.Config() // Access configuration
|
||||
func (b *Buffered[H]) Config() *Buffering {
|
||||
return b.config
|
||||
}
|
||||
|
||||
// worker processes entries and handles flushing.
|
||||
// It runs in a goroutine, buffering entries, flushing on batch size, timer, or explicit signal,
|
||||
// and shutting down cleanly when signaled.
|
||||
// Thread-safe via channel operations and WaitGroup.
|
||||
func (b *Buffered[H]) worker() {
|
||||
defer b.wg.Done() // Signal completion when worker exits
|
||||
batch := make([]*lx.Entry, 0, b.config.BatchSize) // Buffer for batching entries
|
||||
ticker := time.NewTicker(b.config.FlushInterval) // Timer for periodic flushes
|
||||
defer ticker.Stop() // Clean up ticker
|
||||
for {
|
||||
select {
|
||||
case entry := <-b.entries: // Receive new entry
|
||||
batch = append(batch, entry)
|
||||
// Flush if batch size is reached
|
||||
if len(batch) >= b.config.BatchSize {
|
||||
b.flushBatch(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
case <-ticker.C: // Periodic flush
|
||||
if len(batch) > 0 {
|
||||
b.flushBatch(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
case <-b.flushSignal: // Explicit flush
|
||||
if len(batch) > 0 {
|
||||
b.flushBatch(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
b.drainRemaining() // Drain all entries from the channel
|
||||
case <-b.shutdown: // Shutdown signal
|
||||
if len(batch) > 0 {
|
||||
b.flushBatch(batch)
|
||||
}
|
||||
b.drainRemaining() // Flush remaining entries
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushBatch processes a batch of entries through the wrapped handler.
|
||||
// It writes each entry to the underlying handler, logging any errors to the configured ErrorOutput.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// b.flushBatch([]*lx.Entry{entry1, entry2})
|
||||
func (b *Buffered[H]) flushBatch(batch []*lx.Entry) {
|
||||
for _, entry := range batch {
|
||||
// Process each entry through the handler
|
||||
if err := b.handler.Handle(entry); err != nil {
|
||||
if b.config.ErrorOutput != nil {
|
||||
fmt.Fprintf(b.config.ErrorOutput, "log flush error: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drainRemaining processes any remaining entries in the channel.
|
||||
// It flushes all entries from the entries channel to the underlying handler,
|
||||
// logging any errors to the configured ErrorOutput. Used during flush or shutdown.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// b.drainRemaining() // Flushes all pending entries
|
||||
func (b *Buffered[H]) drainRemaining() {
|
||||
for {
|
||||
select {
|
||||
case entry := <-b.entries: // Process next entry
|
||||
if err := b.handler.Handle(entry); err != nil {
|
||||
if b.config.ErrorOutput != nil {
|
||||
fmt.Fprintf(b.config.ErrorOutput, "log drain error: %v\n", err)
|
||||
}
|
||||
}
|
||||
default: // Exit when channel is empty
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
+1063
File diff suppressed because it is too large
Load Diff
+163
@@ -0,0 +1,163 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// Dedup is a log handler that suppresses duplicate entries within a TTL window.
|
||||
// It wraps another handler (H) and filters out repeated log entries that match
|
||||
// within the deduplication period.
|
||||
type Dedup[H lx.Handler] struct {
|
||||
next H
|
||||
|
||||
ttl time.Duration
|
||||
cleanupEvery time.Duration
|
||||
keyFn func(*lx.Entry) uint64
|
||||
maxKeys int
|
||||
|
||||
// shards reduce lock contention by partitioning the key space
|
||||
shards [32]dedupShard
|
||||
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
type dedupShard struct {
|
||||
mu sync.Mutex
|
||||
seen map[uint64]int64
|
||||
}
|
||||
|
||||
// DedupOpt configures a Dedup handler.
|
||||
type DedupOpt[H lx.Handler] func(*Dedup[H])
|
||||
|
||||
// WithDedupKeyFunc customizes how deduplication keys are generated.
|
||||
func WithDedupKeyFunc[H lx.Handler](fn func(*lx.Entry) uint64) DedupOpt[H] {
|
||||
return func(d *Dedup[H]) { d.keyFn = fn }
|
||||
}
|
||||
|
||||
// WithDedupCleanupInterval sets how often expired deduplication keys are purged.
|
||||
func WithDedupCleanupInterval[H lx.Handler](every time.Duration) DedupOpt[H] {
|
||||
return func(d *Dedup[H]) {
|
||||
if every > 0 {
|
||||
d.cleanupEvery = every
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithDedupMaxKeys sets a soft limit on tracked deduplication keys.
|
||||
func WithDedupMaxKeys[H lx.Handler](max int) DedupOpt[H] {
|
||||
return func(d *Dedup[H]) {
|
||||
if max > 0 {
|
||||
d.maxKeys = max
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewDedup creates a deduplicating handler wrapper.
|
||||
func NewDedup[H lx.Handler](next H, ttl time.Duration, opts ...DedupOpt[H]) *Dedup[H] {
|
||||
if ttl <= 0 {
|
||||
ttl = 2 * time.Second
|
||||
}
|
||||
|
||||
d := &Dedup[H]{
|
||||
next: next,
|
||||
ttl: ttl,
|
||||
cleanupEvery: time.Minute,
|
||||
keyFn: defaultDedupKey,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Initialize shards
|
||||
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[H]) Handle(e *lx.Entry) error {
|
||||
now := time.Now().UnixNano()
|
||||
key := d.keyFn(e)
|
||||
|
||||
// Select shard based on key hash
|
||||
shardIdx := key % uint64(len(d.shards))
|
||||
shard := &d.shards[shardIdx]
|
||||
|
||||
shard.mu.Lock()
|
||||
exp, ok := shard.seen[key]
|
||||
if ok && now < exp {
|
||||
shard.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Basic guard against unbounded growth per shard
|
||||
// Using strict limits per shard avoids global atomic counters
|
||||
limitPerShard := d.maxKeys / len(d.shards)
|
||||
if d.maxKeys > 0 && len(shard.seen) >= limitPerShard {
|
||||
// Opportunistic cleanup of current shard
|
||||
d.cleanupShard(shard, now)
|
||||
}
|
||||
|
||||
shard.seen[key] = now + d.ttl.Nanoseconds()
|
||||
shard.mu.Unlock()
|
||||
|
||||
return d.next.Handle(e)
|
||||
}
|
||||
|
||||
// Close stops the cleanup goroutine and closes the underlying handler.
|
||||
func (d *Dedup[H]) Close() error {
|
||||
var err error
|
||||
d.once.Do(func() {
|
||||
close(d.done)
|
||||
d.wg.Wait()
|
||||
|
||||
if c, ok := any(d.next).(interface{ Close() error }); ok {
|
||||
err = c.Close()
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// cleanupLoop runs periodically to purge expired deduplication keys.
|
||||
func (d *Dedup[H]) cleanupLoop() {
|
||||
defer d.wg.Done()
|
||||
|
||||
t := time.NewTicker(d.cleanupEvery)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
now := time.Now().UnixNano()
|
||||
// Cleanup all shards sequentially to avoid massive CPU spike
|
||||
for i := 0; i < len(d.shards); i++ {
|
||||
d.shards[i].mu.Lock()
|
||||
d.cleanupShard(&d.shards[i], now)
|
||||
d.shards[i].mu.Unlock()
|
||||
}
|
||||
case <-d.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupShard removes expired keys from a specific shard.
|
||||
func (d *Dedup[H]) cleanupShard(shard *dedupShard, now int64) {
|
||||
for k, exp := range shard.seen {
|
||||
if now > exp {
|
||||
delete(shard.seen, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
var jsonBufPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
// JSONHandler is a handler that outputs log entries as JSON objects.
|
||||
// It formats log entries with timestamp, level, message, namespace, fields, and optional
|
||||
// stack traces or dump segments, writing the result to the provided writer.
|
||||
// Thread-safe with a mutex to protect concurrent writes.
|
||||
type JSONHandler struct {
|
||||
writer io.Writer // Destination for JSON output
|
||||
timeFmt string // Format for timestamp (default: RFC3339Nano)
|
||||
pretty bool // Enable pretty printing with indentation if true
|
||||
//fieldMap map[string]string // Optional mapping for field names (not used in provided code)
|
||||
mu sync.Mutex // Protects concurrent access to writer
|
||||
}
|
||||
|
||||
// JsonOutput represents the JSON structure for a log entry.
|
||||
// It includes all relevant log data, such as timestamp, level, message, and optional
|
||||
// stack trace or dump segments, serialized as a JSON object.
|
||||
type JsonOutput struct {
|
||||
Time string `json:"ts"` // Timestamp in specified format
|
||||
Level string `json:"lvl"` // Log level (e.g., "INFO")
|
||||
Class string `json:"class"` // Entry class (e.g., "Text", "Dump")
|
||||
Msg string `json:"msg"` // Log message
|
||||
Namespace string `json:"ns"` // Namespace path
|
||||
Stack []byte `json:"stack"` // Stack trace (if present)
|
||||
Dump []dumpSegment `json:"dump"` // Hex/ASCII dump segments (for ClassDump)
|
||||
Fields map[string]interface{} `json:"fields"` // Custom fields
|
||||
}
|
||||
|
||||
// dumpSegment represents a single segment of a hex/ASCII dump.
|
||||
// Used for ClassDump entries to structure position, hex values, and ASCII representation.
|
||||
type dumpSegment struct {
|
||||
Offset int `json:"offset"` // Starting byte offset of the segment
|
||||
Hex []string `json:"hex"` // Hexadecimal values of bytes
|
||||
ASCII string `json:"ascii"` // ASCII representation of bytes
|
||||
}
|
||||
|
||||
// NewJSONHandler creates a new JSONHandler writing to the specified writer.
|
||||
// It initializes the handler with a default timestamp format (RFC3339Nano) and optional
|
||||
// configuration functions to customize settings like pretty printing.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewJSONHandler(os.Stdout)
|
||||
// logger := ll.New("app").Enable().Handler(handler)
|
||||
// logger.Info("Test") // Output: {"ts":"...","lvl":"INFO","class":"Text","msg":"Test","ns":"app","stack":null,"dump":null,"fields":null}
|
||||
func NewJSONHandler(w io.Writer, opts ...func(*JSONHandler)) *JSONHandler {
|
||||
h := &JSONHandler{
|
||||
writer: w, // Set output writer
|
||||
timeFmt: time.RFC3339Nano, // Default timestamp format
|
||||
}
|
||||
// Apply configuration options
|
||||
for _, opt := range opts {
|
||||
opt(h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Handle processes a log entry and writes it as JSON.
|
||||
// It delegates to specialized methods based on the entry's class (Dump or regular),
|
||||
// ensuring thread-safety with a mutex.
|
||||
// Returns an error if JSON encoding or writing fails.
|
||||
// Example:
|
||||
//
|
||||
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes JSON object
|
||||
func (h *JSONHandler) Handle(e *lx.Entry) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// Handle dump entries separately
|
||||
if e.Class == lx.ClassDump {
|
||||
return h.handleDump(e)
|
||||
}
|
||||
// Handle standard log entries
|
||||
return h.handleRegular(e)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Convert ordered fields to map for JSON output
|
||||
fieldsMap := make(map[string]interface{}, len(e.Fields))
|
||||
for _, pair := range e.Fields {
|
||||
fieldsMap[pair.Key] = pair.Value
|
||||
}
|
||||
|
||||
// Create JSON output structure
|
||||
entry := JsonOutput{
|
||||
Time: e.Timestamp.Format(h.timeFmt), // Format timestamp
|
||||
Level: e.Level.String(), // Convert level to string
|
||||
Class: e.Class.String(), // Convert class to string
|
||||
Msg: e.Message, // Set message
|
||||
Namespace: e.Namespace, // Set namespace
|
||||
Dump: nil, // No dump for regular entries
|
||||
Fields: fieldsMap, // Copy fields as map
|
||||
Stack: e.Stack, // Include stack trace if present
|
||||
}
|
||||
|
||||
// Acquire buffer from pool to avoid allocation and reduce syscalls
|
||||
buf := jsonBufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer jsonBufPool.Put(buf)
|
||||
|
||||
// Create JSON encoder writing to buffer
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// Convert ordered fields to map for JSON output
|
||||
fieldsMap := make(map[string]interface{}, len(e.Fields))
|
||||
for _, pair := range e.Fields {
|
||||
fieldsMap[pair.Key] = pair.Value
|
||||
}
|
||||
|
||||
// Acquire buffer from pool
|
||||
buf := jsonBufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer jsonBufPool.Put(buf)
|
||||
|
||||
// Encode JSON output with dump segments to buffer
|
||||
enc := json.NewEncoder(buf)
|
||||
if h.pretty {
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
|
||||
err := enc.Encode(JsonOutput{
|
||||
Time: e.Timestamp.Format(h.timeFmt), // Format timestamp
|
||||
Level: e.Level.String(), // Convert level to string
|
||||
Class: e.Class.String(), // Convert class to string
|
||||
Msg: "dumping segments", // Fixed message for dumps
|
||||
Namespace: e.Namespace, // Set namespace
|
||||
Dump: segments, // Include parsed segments
|
||||
Fields: fieldsMap, // Copy fields as map
|
||||
Stack: e.Stack, // Include stack trace if present
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
var dedupBufPool = sync.Pool{
|
||||
New: func() any { return new(bytes.Buffer) },
|
||||
}
|
||||
|
||||
// defaultDedupKey generates a deduplication key from log level and message.
|
||||
// Uses FNV-1a hash for speed and good distribution. Override with WithDedupKeyFunc
|
||||
// to include additional fields like namespace, caller, or structured fields.
|
||||
func defaultDedupKey(e *lx.Entry) uint64 {
|
||||
h := xxhash.New()
|
||||
|
||||
_, _ = h.Write([]byte(e.Level.String()))
|
||||
_, _ = h.Write([]byte{0})
|
||||
_, _ = h.Write([]byte(e.Message))
|
||||
_, _ = h.Write([]byte{0})
|
||||
_, _ = h.Write([]byte(e.Namespace))
|
||||
_, _ = h.Write([]byte{0})
|
||||
|
||||
if len(e.Fields) > 0 {
|
||||
m := e.Fields.Map()
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
buf := dedupBufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer dedupBufPool.Put(buf)
|
||||
|
||||
for _, k := range keys {
|
||||
fmt.Fprint(buf, k)
|
||||
buf.WriteByte('=')
|
||||
fmt.Fprint(buf, m[k])
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
_, _ = h.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
return h.Sum64()
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package 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
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package 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...)
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package 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.Handler]) 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
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
// // Rename current log before creating 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 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 cleanup/rotation actions before opening a new destination.
|
||||
// For files: rename or move the current log. Optional for other destinations.
|
||||
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 io.WriteCloser
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
// rotateIfNeededLocked checks current size and rotates if maxSize exceeded.
|
||||
// Called with mu already held.
|
||||
func (r *Rotating[H]) rotateIfNeededLocked() error {
|
||||
if r.maxSize <= 0 || r.src.Size == nil || r.src.Open == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
size, err := r.src.Size()
|
||||
if err != nil {
|
||||
// Size unknown - skip rotation
|
||||
return nil
|
||||
}
|
||||
if size < r.maxSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close current output
|
||||
if r.out != nil {
|
||||
_ = r.out.Close()
|
||||
r.out = nil
|
||||
}
|
||||
|
||||
// Run rotation hook (rename/move/commit)
|
||||
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
|
||||
}
|
||||
r.out = out
|
||||
r.handler.Output(out)
|
||||
return nil
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package 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
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"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("=")
|
||||
fmt.Fprint(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")
|
||||
}
|
||||
+1457
File diff suppressed because it is too large
Load Diff
+140
@@ -0,0 +1,140 @@
|
||||
package 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()
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package 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
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package 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)
|
||||
)
|
||||
|
||||
// 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"
|
||||
)
|
||||
|
||||
// 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
|
||||
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]
|
||||
)
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package lx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// namespaceRule stores the cached result of Enabled.
|
||||
type namespaceRule struct {
|
||||
isEnabledByRule bool
|
||||
isDisabledByRule bool
|
||||
}
|
||||
|
||||
// Namespace manages thread-safe namespace enable/disable states with caching.
|
||||
// The store holds explicit user-defined rules (path -> bool).
|
||||
// The cache holds computed effective states for paths (path -> namespaceRule)
|
||||
// based on hierarchical rules to optimize lookups.
|
||||
type Namespace struct {
|
||||
store sync.Map // path (string) -> rule (bool: true=enable, false=disable)
|
||||
cache sync.Map // path (string) -> namespaceRule
|
||||
}
|
||||
|
||||
// Set defines an explicit enable/disable rule for a namespace path.
|
||||
// It clears the cache to ensure subsequent lookups reflect the change.
|
||||
func (ns *Namespace) Set(path string, enabled bool) {
|
||||
ns.store.Store(path, enabled)
|
||||
ns.clearCache()
|
||||
}
|
||||
|
||||
// Load retrieves an explicit rule from the store for a path.
|
||||
// Returns the rule (true=enable, false=disable) and whether it exists.
|
||||
// Does not consider hierarchy or caching.
|
||||
func (ns *Namespace) Load(path string) (rule interface{}, found bool) {
|
||||
return ns.store.Load(path)
|
||||
}
|
||||
|
||||
// Store directly sets a rule in the store, bypassing cache invalidation.
|
||||
// Intended for internal use or sync.Map parity; prefer Set for standard use.
|
||||
func (ns *Namespace) Store(path string, rule bool) {
|
||||
ns.store.Store(path, rule)
|
||||
}
|
||||
|
||||
// clearCache clears the cache of Enabled results.
|
||||
// Called by Set to ensure consistency after rule changes.
|
||||
func (ns *Namespace) clearCache() {
|
||||
ns.cache.Range(func(key, _ interface{}) bool {
|
||||
ns.cache.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Enabled checks if a path is enabled by namespace rules, considering the most
|
||||
// specific rule (path or closest prefix) in the store. Results are cached.
|
||||
// Args:
|
||||
// - path: Absolute namespace path to check.
|
||||
// - separator: Character delimiting path segments (e.g., "/", ".").
|
||||
//
|
||||
// Returns:
|
||||
// - isEnabledByRule: True if an explicit rule enables the path.
|
||||
// - isDisabledByRule: True if an explicit rule disables the path.
|
||||
//
|
||||
// If both are false, no explicit rule applies to the path or its prefixes.
|
||||
func (ns *Namespace) Enabled(path string, separator string) (isEnabledByRule bool, isDisabledByRule bool) {
|
||||
if path == "" { // Root path has no explicit rule
|
||||
return false, false
|
||||
}
|
||||
|
||||
// Check cache
|
||||
if cachedValue, found := ns.cache.Load(path); found {
|
||||
if state, ok := cachedValue.(namespaceRule); ok {
|
||||
return state.isEnabledByRule, state.isDisabledByRule
|
||||
}
|
||||
ns.cache.Delete(path) // Remove invalid cache entry
|
||||
}
|
||||
|
||||
// Compute: Most specific rule wins
|
||||
parts := strings.Split(path, separator)
|
||||
computedIsEnabled := false
|
||||
computedIsDisabled := false
|
||||
|
||||
for i := len(parts); i >= 1; i-- {
|
||||
currentPrefix := strings.Join(parts[:i], separator)
|
||||
if val, ok := ns.store.Load(currentPrefix); ok {
|
||||
if rule := val.(bool); rule {
|
||||
computedIsEnabled = true
|
||||
computedIsDisabled = false
|
||||
} else {
|
||||
computedIsEnabled = false
|
||||
computedIsDisabled = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Cache result, including (false, false) for no rule
|
||||
ns.cache.Store(path, namespaceRule{
|
||||
isEnabledByRule: computedIsEnabled,
|
||||
isDisabledByRule: computedIsDisabled,
|
||||
})
|
||||
|
||||
return computedIsEnabled, computedIsDisabled
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package lx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LevelType represents the severity of a log message.
|
||||
// It is an integer type used to define log levels (Debug, Info, Warn, Error, None), with associated
|
||||
// string representations for display in log output.
|
||||
type LevelType int
|
||||
|
||||
// String converts a LevelType to its string representation.
|
||||
// It maps each level constant to a human-readable string, returning "UNKNOWN" for invalid levels.
|
||||
// Used by handlers to display the log level in output.
|
||||
// Example:
|
||||
//
|
||||
// var level lx.LevelType = lx.LevelInfo
|
||||
// fmt.Println(level.String()) // Output: INFO
|
||||
func (l LevelType) String() string {
|
||||
switch l {
|
||||
case LevelDebug:
|
||||
return DebugString
|
||||
case LevelInfo:
|
||||
return InfoString
|
||||
case LevelWarn:
|
||||
return WarnString
|
||||
case LevelError:
|
||||
return ErrorString
|
||||
case LevelFatal:
|
||||
return FatalString
|
||||
case LevelNone:
|
||||
return NoneString
|
||||
default:
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package ll
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// Middleware represents a registered middleware and its operations in the logging pipeline.
|
||||
// It holds an ID for identification, a reference to the parent logger, and the handler function
|
||||
// that processes log entries. Middleware is used to transform or filter log entries before they
|
||||
// are passed to the logger's output handler.
|
||||
type Middleware struct {
|
||||
id int // Unique identifier for the middleware
|
||||
logger *Logger // Parent logger instance for context and logging operations
|
||||
fn lx.Handler // Handler function that processes log entries
|
||||
}
|
||||
|
||||
// Remove unregisters the middleware from the logger’s middleware chain.
|
||||
// It safely removes the middleware by its ID, ensuring thread-safety with a mutex lock.
|
||||
// If the middleware or logger is nil, it returns early to prevent panics.
|
||||
// Example usage:
|
||||
//
|
||||
// // Using a named middleware function
|
||||
// mw := logger.Use(authMiddleware)
|
||||
// defer mw.Remove()
|
||||
//
|
||||
// // Using an inline middleware
|
||||
// mw = logger.Use(ll.Middle(func(e *lx.Entry) error {
|
||||
// if e.Level < lx.LevelWarn {
|
||||
// return fmt.Errorf("level too low")
|
||||
// }
|
||||
// return nil
|
||||
// }))
|
||||
// defer mw.Remove()
|
||||
func (m *Middleware) Remove() {
|
||||
// Check for nil middleware or logger to avoid panics
|
||||
if m == nil || m.logger == nil {
|
||||
return
|
||||
}
|
||||
// Acquire write lock to modify middleware slice
|
||||
m.logger.mu.Lock()
|
||||
defer m.logger.mu.Unlock()
|
||||
// Iterate through middleware slice to find and remove matching ID
|
||||
for i, entry := range m.logger.middleware {
|
||||
if entry.id == m.id {
|
||||
// Remove middleware by slicing out the matching entry
|
||||
m.logger.middleware = append(m.logger.middleware[:i], m.logger.middleware[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logger returns the parent logger for optional chaining.
|
||||
// This allows middleware to access the logger for additional operations, such as logging errors
|
||||
// or creating derived loggers. It is useful for fluent API patterns.
|
||||
// Example:
|
||||
//
|
||||
// mw := logger.Use(authMiddleware)
|
||||
// mw.Logger().Info("Middleware registered")
|
||||
func (m *Middleware) Logger() *Logger {
|
||||
return m.logger
|
||||
}
|
||||
|
||||
// Error logs an error message at the Error level if the middleware blocks a log entry.
|
||||
// It uses the parent logger to emit the error and returns the middleware for chaining.
|
||||
// This is useful for debugging or auditing when middleware rejects a log.
|
||||
// Example:
|
||||
//
|
||||
// mw := logger.Use(ll.Middle(func(e *lx.Entry) error {
|
||||
// if e.Level < lx.LevelWarn {
|
||||
// return fmt.Errorf("level too low")
|
||||
// }
|
||||
// return nil
|
||||
// }))
|
||||
// mw.Error("Rejected low-level log")
|
||||
func (m *Middleware) Error(args ...any) *Middleware {
|
||||
m.logger.Error(args...)
|
||||
return m
|
||||
}
|
||||
|
||||
// Errorf logs an error message at the Error level if the middleware blocks a log entry.
|
||||
// It uses the parent logger to emit the error and returns the middleware for chaining.
|
||||
// This is useful for debugging or auditing when middleware rejects a log.
|
||||
// Example:
|
||||
//
|
||||
// mw := logger.Use(ll.Middle(func(e *lx.Entry) error {
|
||||
// if e.Level < lx.LevelWarn {
|
||||
// return fmt.Errorf("level too low")
|
||||
// }
|
||||
// return nil
|
||||
// }))
|
||||
// mw.Errorf("Rejected low-level log")
|
||||
func (m *Middleware) Errorf(format string, args ...any) *Middleware {
|
||||
m.logger.Errorf(format, args...)
|
||||
return m
|
||||
}
|
||||
|
||||
// middlewareFunc is a function adapter that implements the lx.Handler interface.
|
||||
// It allows plain functions with the signature `func(*lx.Entry) error` to be used as middleware.
|
||||
// The function should return nil to allow the log to proceed or a non-nil error to reject it,
|
||||
// stopping the log from being emitted by the logger.
|
||||
type middlewareFunc func(*lx.Entry) error
|
||||
|
||||
// Handle implements the lx.Handler interface for middlewareFunc.
|
||||
// It calls the underlying function with the log entry and returns its result.
|
||||
// This enables seamless integration of function-based middleware into the logging pipeline.
|
||||
func (mf middlewareFunc) Handle(e *lx.Entry) error {
|
||||
return mf(e)
|
||||
}
|
||||
|
||||
// Middle creates a middleware handler from a function.
|
||||
// It wraps a function with the signature `func(*lx.Entry) error` into a middlewareFunc,
|
||||
// allowing it to be used in the logger’s middleware pipeline. A non-nil error returned by
|
||||
// the function will stop the log from being emitted, ensuring precise control over logging.
|
||||
// Example:
|
||||
//
|
||||
// logger.Use(ll.Middle(func(e *lx.Entry) error {
|
||||
// if e.Level == lx.LevelDebug {
|
||||
// return fmt.Errorf("debug logs disabled")
|
||||
// }
|
||||
// return nil
|
||||
// }))
|
||||
func Middle(fn func(*lx.Entry) error) lx.Handler {
|
||||
return middlewareFunc(fn)
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package 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
|
||||
}
|
||||
}
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
package 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)
|
||||
}
|
||||
Reference in New Issue
Block a user