diff --git a/README.md b/README.md index ca6f24c..ca53e83 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,121 @@ # log -Transitland wrapper for zerolog. \ No newline at end of file +A lightweight logging wrapper for [zerolog](https://github.com/rs/zerolog), providing convenience functions and sensible defaults for [Interline](https://www.interline.io/) and [Transitland](https://www.transit.land/) projects. + +> **Note:** This library is primarily intended for internal use across Interline/Transitland repositories—mainly for our convenience and to avoid having `zerolog.Xyz` scattered throughout our codebase and copy-pasting logging middlewares. It's published publicly for our convenience; external users should probably just use zerolog directly. + +## Features + +- Simple printf-style logging functions (`Infof`, `Debugf`, `Tracef`, `Errorf`) +- Structured logging via zerolog's fluent API +- Pretty console output with colors (automatically disabled when piping/redirecting) +- JSON output mode for production environments +- Context-aware logging with request ID support +- HTTP middleware for request logging + +## Installation + +```bash +go get github.com/interline-io/log +``` + +## Usage + +### Basic Logging + +```go +package main + +import "github.com/interline-io/log" + +func main() { + // Printf-style logging + log.Infof("Processing %d items", 42) + log.Debugf("Debug info: %s", "details") + log.Errorf("Something went wrong: %v", err) + log.Tracef("Verbose trace: %+v", obj) + + // Structured logging (zerolog style) + log.Info().Str("user", "alice").Int("count", 5).Msg("User action") + log.Error().Err(err).Str("op", "save").Msg("Operation failed") + + // Simple print (no timestamp, ignores log level) + log.Print("Plain output: %s", "hello") +} +``` + +### Context-Aware Logging + +```go +// Get logger from context +logger := log.For(ctx) +logger.Info().Msg("Using context logger") + +// Add logger to context +ctx = log.WithLogger(ctx, customLogger) +``` + +### HTTP Middleware + +```go +import "github.com/interline-io/log" + +// Add request ID to each request +r.Use(log.RequestIDMiddleware) + +// Add request ID to context logger +r.Use(log.RequestIDLoggingMiddleware) + +// Log request duration and details +r.Use(log.LoggingMiddleware(1000, getUserNameFunc)) +``` + +## Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `TL_LOG` | Log level: `TRACE`, `DEBUG`, `INFO`, `ERROR`, `FATAL` | `INFO` | +| `TL_LOG_JSON` | Set to `true` for JSON output (no colors) | `false` | + +### Examples + +```bash +# Enable debug logging +TL_LOG=DEBUG ./myapp + +# Enable trace logging (most verbose) +TL_LOG=TRACE ./myapp + +# JSON output for production/log aggregation +TL_LOG_JSON=true ./myapp +``` + +### Terminal Colors + +Console output includes ANSI colors when writing to a terminal. Colors are automatically disabled when output is piped or redirected: + +```bash +# Colors enabled (terminal) +./myapp + +# Colors disabled (piped) +./myapp | tee output.log + +# Colors disabled (redirected) +./myapp > output.log 2>&1 +``` + +## Log Levels + +| Level | Function | Use Case | +|-------|----------|----------| +| `TRACE` | `Tracef()`, `Trace()` | Verbose debugging, performance tracing | +| `DEBUG` | `Debugf()`, `Debug()` | Development debugging | +| `INFO` | `Infof()`, `Info()` | General operational messages | +| `ERROR` | `Errorf()`, `Error()` | Error conditions | + +## License + +See [LICENSE](LICENSE) file. \ No newline at end of file diff --git a/cmd/testlog/main.go b/cmd/testlog/main.go new file mode 100644 index 0000000..f94de78 --- /dev/null +++ b/cmd/testlog/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/interline-io/log" +) + +func main() { + log.Print("This is a plain print (no level, no timestamp)") + + log.Tracef("This is a trace message: %d", 1) + log.Debugf("This is a debug message: %d", 2) + log.Infof("This is an info message: %d", 3) + log.Errorf("This is an error message: %d", 4) + + log.Traceln("Traceln:", "multiple", "args", 123) + + log.Info().Str("key", "value").Int("count", 42).Msg("Structured logging example") + log.Debug().Str("user", "alice").Bool("active", true).Msg("User status") + log.Error().Err(nil).Str("op", "test").Msg("Error with nil error") + + log.Print("Done!") +} diff --git a/cmd/testlog/test.sh b/cmd/testlog/test.sh new file mode 100755 index 0000000..754ab05 --- /dev/null +++ b/cmd/testlog/test.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +echo "=== Direct to terminal (should have colors) ===" +TL_LOG=TRACE go run main.go + +echo "" +echo "=== Piped through cat (no colors) ===" +TL_LOG=TRACE go run main.go 2>&1 | cat + +echo "" +echo "=== Piped through tee (no colors) ===" +TL_LOG=TRACE go run main.go 2>&1 | tee /tmp/testlog_output.txt + +echo "" +echo "=== Redirected to file (no colors) ===" +TL_LOG=TRACE go run main.go > /tmp/testlog_redirect.txt 2>&1 +cat /tmp/testlog_redirect.txt + +echo "" +echo "=== JSON mode (TL_LOG_JSON=true) ===" +TL_LOG=TRACE TL_LOG_JSON=true go run main.go + +echo "" +echo "=== Default log level (INFO) ===" +go run main.go diff --git a/go.mod b/go.mod index de96f7c..f19a40b 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/interline-io/log -go 1.20 +go 1.24.0 require github.com/rs/zerolog v1.31.0 require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect ) diff --git a/go.sum b/go.sum index 7349483..e5fdfc5 100644 --- a/go.sum +++ b/go.sum @@ -13,3 +13,7 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= diff --git a/log.go b/log.go index ae708a6..b25a549 100644 --- a/log.go +++ b/log.go @@ -7,6 +7,7 @@ import ( "time" "github.com/rs/zerolog" + "golang.org/x/term" ) // Zerolog @@ -18,7 +19,7 @@ func With() zerolog.Context { } func Fatal() *zerolog.Event { - return Logger.Info() + return Logger.Fatal() } func Info() *zerolog.Event { @@ -88,7 +89,11 @@ func SetLevel(lvalue zerolog.Level) { if !jsonLog { // use console logging zerolog.TimeFieldFormat = zerolog.TimeFormatUnix - output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339} + output := zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + NoColor: !term.IsTerminal(int(os.Stdout.Fd())), + } output.FormatLevel = func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("[%-5s]", i)) }