Fast and feature-rich structured logging library for Go
- Features
- Production Tested
- Installation
- Quick Start
- Log Levels
- Sub-loggers and Context
- Multiple Writers and Filtering
- Terminal Detection
- Ready-to-Use Logger (
logsubpackage) - Rotating Log Files
- Standard Library Integration (slog)
- HTTP Middleware
- Advanced Features
- Performance
- Comparison with Other Logging Libraries
- API Reference
- License
- High Performance: Zero-allocation logging for JSON, text, and complex fields (error, time.Time)
- Structured Logging: Type-safe field methods for all Go primitives including native time.Time and UUID
- Tag-driven Struct Logging:
StructFieldsandTaggedStructFieldshonorgolog,log, andjsontags withomitempty,omitzero,omitnull, andredactmodifiers.encoding/json-compatible where applicable - Multiple Output Formats: JSON and human-readable text output
- Terminal Auto-Detection: Automatically switches between colored text (TTY) and JSON (non-TTY)
- Configurable Log Levels: TRACE, DEBUG, INFO, WARN, ERROR, FATAL with flexible filtering
- Context Support: Log attributes can be stored in and retrieved from context automatically
- Duplicate Key Prevention: A message attribute is dropped when the same key was already set by the logger, a sub-logger, or the context (the inherited value wins), keeping inherited structured data clean
- Colorized Output: Beautiful colored console output with customizable colorizers
- Multi-Writer Architecture: Log to multiple destinations with different formats and filters
- Rotating Log Files: Automatic file rotation based on size thresholds
- slog Integration: Use as a backend for Go's standard log/slog package
- HTTP Middleware: Built-in HTTP request/response logging with request ID propagation
- UUID Support: Native UUID logging with zero allocations
- Call Stack Tracing: Capture and log call stacks for debugging
- Memory Safety: Nil-safe logger implementation prevents panics
- Sub-loggers: Create child loggers with inherited attributes
golog has been used in production at domonda since 2023, powering mission-critical business applications with reliable, high-performance logging.
go get github.com/domonda/gologpackage main
import (
"errors"
"os"
"github.com/domonda/golog"
)
func main() {
// Create a basic text logger
config := golog.NewConfig(
&golog.DefaultLevels,
golog.AllLevelsActive,
golog.NewTextWriterConfig(os.Stdout, nil, nil),
)
log := golog.NewLogger(config)
// Simple logging
log.Info("Hello, World!").Log()
log.Error("Something went wrong").Err(errors.New("example error")).Log()
}// Create a JSON logger
config := golog.NewConfig(
&golog.DefaultLevels,
golog.AllLevelsActive,
golog.NewJSONWriterConfig(os.Stdout, nil),
)
log := golog.NewLogger(config)
start := time.Now()
// ... perform login ...
log.Info("User login").
Str("username", "john_doe").
Str("ip", "192.168.1.1").
Duration("login_time", time.Since(start)).
Log()Output (matches NewDefaultFormat: "time" key, "2006-01-02 15:04:05.000" layout):
{"time":"2024-01-15 10:30:45.000","level":"INFO","message":"User login","username":"john_doe","ip":"192.168.1.1","login_time":"150ms"}requestID := golog.UUIDv4() // [16]byte UUID
jsonBytes := []byte(`{"key":"value"}`) // Raw JSON
log.Info("Processing request").
Str("method", "POST").
Str("path", "/api/users").
Int("user_id", 12345).
Bool("authenticated", true).
Float("response_time", 0.145).
UUID("request_id", requestID).
Time("started_at", time.Now()).
Strs("tags", []string{"api", "user", "create"}).
JSON("metadata", jsonBytes).
Log()golog supports six standard log levels:
- TRACE (-20): Most verbose, for tracing execution flow
- DEBUG (-10): Debug information for development
- INFO (0): General information messages
- WARN (10): Warning messages for potentially harmful situations
- ERROR (20): Error conditions that don't require immediate attention
- FATAL (30): Critical errors that may cause application termination
log.Trace("Entering function").Str("function", "processData").Log()
log.Debug("Variable state").Int("counter", 42).Log()
log.Info("Operation completed").Log()
log.Warn("Deprecated API used").Str("api", "/old/endpoint").Log()
log.Error("Failed to connect").Err(err).Log()
log.Fatal("Critical system failure").Log()requestID := golog.UUIDv4()
// Create a sub-logger with common attributes
subLog := log.With().
Str("service", "user-management").
UUID("request_id", requestID).
SubLogger()
// All logs from subLog will include the above attributes
subLog.Info("User created").Str("username", "john").Log()
subLog.Error("User validation failed").Err(errors.New("invalid email")).Log()// Add attributes to context
ctx = golog.ContextWithAttribs(ctx,
golog.NewString("correlation_id", "abc-123"),
golog.NewString("user_id", "user-456"),
)
// Create logger from context
ctxLogger := log.WithCtx(ctx)
ctxLogger.Info("Operation started").Log() // Includes context attributes
// Or use the logger's InfoCtx method
logger.InfoCtx(ctx, "Operation started").Log() // Includes context attributeslogFile, _ := os.Create("app.log")
defer logFile.Close()
// Log to multiple outputs with different formats
config := golog.NewConfig(
&golog.DefaultLevels,
golog.AllLevelsActive,
golog.NewTextWriterConfig(os.Stdout, nil, golog.NewStyledColorizer()),
// FilterOutBelow filters out less severe levels (lower numeric values)
// Warn=10, so this filters out Trace=-20, Debug=-10, Info=0
// Only Warn=10, Error=20, Fatal=30 will be logged to the file
golog.NewJSONWriterConfig(logFile, nil, golog.DefaultLevels.Warn.FilterOutBelow()),
)
log := golog.NewLogger(config)
// This will appear in colored text on stdout and as JSON in the file (if WARN+)
log.Error("Database connection failed").Err(errors.New("connection refused")).Log()golog can automatically switch between human-readable text and machine-readable JSON output based on whether the process is attached to a terminal (TTY):
config := golog.NewConfig(
&golog.DefaultLevels,
golog.AllLevelsActive,
golog.DecideWriterConfigForTerminal(
golog.NewTextWriterConfig(os.Stdout, nil, golog.NewStyledColorizer()), // Used when running in terminal
golog.NewJSONWriterConfig(os.Stdout, nil), // Used when output is piped/redirected
),
)
log := golog.NewLogger(config)- Terminal (TTY): Outputs colored, human-readable text format
- Non-terminal: Outputs machine-readable JSON format (ideal for log aggregation systems)
You can also check the terminal status directly:
if golog.IsTerminal() {
// Running in a terminal
}This feature is useful for:
- Development: Human-readable logs in the terminal
- Production: Machine-parseable JSON logs for log aggregation systems
- CI/CD: Automatic format switching based on the environment
- Containerized Applications: Proper format when logs are piped to files or log collectors
For quick setup, the log subpackage provides a pre-configured logger with sensible defaults:
import (
"errors"
"github.com/domonda/golog/log"
)
func main() {
log.Info("Application started").Log()
log.Error("Something went wrong").Err(errors.New("example error")).Log()
log.Debug("Debug information").Str("key", "value").Log()
}The log package is configured with:
- Log Levels: Uses
golog.DefaultLevels(TRACE, DEBUG, INFO, WARN, ERROR, FATAL) - Level Filter: Filters out levels below
LOG_LEVELenvironment variable (defaults to DEBUG if not set) - Output: Writes to stdout with automatic format selection:
- Terminal: Colorized text format for human readability
- Non-terminal: JSON format for log aggregation systems
The log.Config variable can be modified at runtime:
import "github.com/domonda/golog/log"
func init() {
// Change the minimum log level
// Info=0, so FilterOutBelow filters out Trace=-20 and Debug=-10
// Only Info=0, Warn=10, Error=20, Fatal=30 will be logged
log.Config = golog.NewConfig(
log.Levels,
log.Levels.Info.FilterOutBelow(),
golog.DecideWriterConfigForTerminal(
golog.NewTextWriterConfig(os.Stdout, &log.Format, &log.Colorizer),
golog.NewJSONWriterConfig(os.Stdout, &log.Format),
),
)
}The logger uses a DerivedConfig that references log.Config, so changes to log.Config take effect immediately without recreating the logger.
The log package exposes convenience functions for all log levels:
log.Trace("message").Log()
log.Debug("message").Log()
log.Info("message").Log()
log.Warn("message").Log()
log.Error("message").Log()
log.Fatal("message").Log()
// With context - attributes added to context are automatically included
ctx := golog.ContextWithAttribs(context.Background(),
golog.NewString("request_id", "req-123"),
golog.NewString("user_id", "user-456"),
)
log.InfoCtx(ctx, "processing request").Log() // Includes request_id and user_id
// Formatted messages
log.Infof("User %s logged in", "john_doe").Log()
// Create sub-loggers
subLog := log.With().Str("component", "auth").SubLogger()Automatic file rotation based on size thresholds using the logfile subpackage:
import (
"github.com/domonda/golog"
"github.com/domonda/golog/logfile"
)
// Create a rotating writer that rotates at 10MB
writer, err := logfile.NewRotatingWriter(
"/var/log/myapp.log", // File path
logfile.RotatingWriterDefaultTimeFormat, // Time format for rotated files
0644, // File permissions
10*1024*1024, // Rotate at 10MB
)
if err != nil {
log.Fatal(err)
}
defer writer.Close()
// Use with golog
config := golog.NewConfig(
&golog.DefaultLevels,
golog.AllLevelsActive,
golog.NewJSONWriterConfig(writer, nil),
)
log := golog.NewLogger(config)
log.Info("Application started").Log()When the log file reaches 10MB:
- The current file is renamed with a timestamp (e.g.,
myapp.log.2024-01-15_10:30:45) - A new file is created at the original path
- Logging continues seamlessly to the new file
// Console output with colors + rotating JSON file
fileWriter, _ := logfile.NewRotatingWriter("/var/log/app.log", "", 0644, 50*1024*1024)
defer fileWriter.Close()
config := golog.NewConfig(
&golog.DefaultLevels,
golog.AllLevelsActive,
golog.NewTextWriterConfig(os.Stdout, nil, golog.NewStyledColorizer()),
golog.NewJSONWriterConfig(fileWriter, nil),
)
log := golog.NewLogger(config)See the logfile package documentation for more details.
Use golog as a backend for Go's standard log/slog package via the goslog adapter:
import (
"log/slog"
"github.com/domonda/golog"
"github.com/domonda/golog/goslog"
)
// Create golog logger
gologLogger := golog.NewLogger(
golog.NewConfig(
&golog.DefaultLevels,
golog.AllLevelsActive,
golog.NewJSONWriterConfig(os.Stdout, nil),
),
)
// Create slog handler that uses golog
handler := goslog.Handler(gologLogger, goslog.ConvertDefaultLevels)
// Use with slog
logger := slog.New(handler)
logger.Info("Hello from slog", "key", "value")- Standard API: Use Go's standard library slog API
- Existing Code: Works with existing code that uses slog
- golog Features: Get all golog benefits (multiple writers, rotation, colors)
- Full Compatibility: Passes slogtest compliance suite
See the goslog package documentation for more details.
func loggingMiddleware(log *golog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Log request
log.Info("HTTP request").
Request(r).
Log()
next.ServeHTTP(w, r)
// Log response
log.Info("HTTP response").
Str("method", r.Method).
Str("path", r.URL.Path).
Duration("duration", time.Since(start)).
Log()
})
}
}colorizer := golog.NewStyledColorizer()
// or implement your own Colorizer interface
config := golog.NewConfig(
&golog.DefaultLevels,
golog.AllLevelsActive,
golog.NewTextWriterConfig(os.Stdout, nil, colorizer),
)defer func() {
if r := recover(); r != nil {
log.Error("Panic recovered").
CallStack("stack").
Any("panic_value", r).
Log()
}
}()StructFields and TaggedStructFields walk a struct via reflection and log
each matching field as an attribute. They are driven entirely by struct tags,
with modifiers that mirror encoding/json.Marshal semantics plus a handful of
golog-specific additions.
StructFields(s) looks for tags in this order — first match wins:
golog:"..."log:"..."json:"..."
Only the first tag present on a field is consulted. Other tags on the same field
are ignored. Fields with none of those tags are skipped entirely (this is a
deliberate difference from encoding/json.Marshal, which would use the Go field
name: golog errs on the side of not leaking untagged internal fields into logs).
TaggedStructFields(s, "json") does the same with a single caller-chosen tag.
Wildcard escape hatch: passing the empty string as the key tag,
TaggedStructFields(s, ""), matches every exported field with no rename and no
modifiers — every field is logged under its Go name, struct tags ignored. Use
this when you want to dump a struct wholesale and you don't care about per-field
tags. (StructFields(s) deliberately stays strict tag-driven; the wildcard is
opt-in.)
Each tag value is name,mod1,mod2,...:
| Tag value | Log key | Notes |
|---|---|---|
"field_name" |
field_name |
explicit name |
"" |
Go field name | empty → fall back to field name |
",omitempty" |
Go field name | empty name with modifier → fall back |
"-" (bare, no comma) |
skipped | only the bare form skips |
"-,..." (comma follows) |
- |
literal field name - + modifiers |
Whitespace around the name and each modifier is trimmed. Unknown modifier
tokens are ignored silently, so json:"name,omitnull" is a valid tag for both
encoding/json.Marshal (which ignores omitnull) and golog (which honors it).
| Modifier | Suppress when… |
|---|---|
omitempty |
false, 0, nil pointer/interface, empty array/slice/map/string. Exactly matches encoding/json.Marshal. Does not catch zero structs or time.Time{}. |
omitzero |
The type's IsZero() bool method returns true, or the value is the Go zero value (via reflect.Value.IsZero). Catches time.Time{} and any zero struct. Strict superset of encoding/json's Go 1.24 omitzero. Both value-receiver and pointer-receiver IsZero methods are honored. |
omitnull |
The type's IsNull() bool method returns true. Falls back to omitzero semantics when the type has no IsNull method. Use for nullable wrappers (golog.Timestamp, sql.NullString, uu.NullableID) where "null" is richer than "all bytes zero". Both value-receiver and pointer-receiver IsNull methods are honored. |
redact |
(not a suppression modifier) — replaces the value with "***REDACTED***" before it reaches the writer. Also spelled redacted. Suppression modifiers win over redact: json:",redact,omitempty" on an empty string emits nothing, not the marker. |
Modifiers OR together — any passing check suppresses the field.
type User struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
APIKey string `json:",redact"`
CreatedAt time.Time `json:"created_at,omitzero"`
DeletedAt golog.Timestamp `json:"deleted_at,omitnull"`
Internal string // no json/log/golog tag → never logged
}
user := User{
ID: 123,
Name: "john_doe",
APIKey: "sk-abc123",
CreatedAt: time.Now(),
// DeletedAt left as zero Timestamp
Internal: "debug-only",
}
log.Info("user").TaggedStructFields(user, "json").Log()
// Output: ... id=123 name="john_doe" APIKey="***REDACTED***" created_at="2026-04-14T..."
// (DeletedAt suppressed by omitnull, Internal never considered.)Breaking changes in this release
log:",redact"(bare, single token) no longer triggers redaction — under the unified parser it names the field"redact". Migrate togolog:",redact"(or combine with other modifiers:golog:",redact,omitempty").StructFields(s)on an untagged struct now logs nothing. Previously it logged every exported field by its Go name. The cleanest replacement isTaggedStructFields(s, ""), the wildcard escape hatch documented above. Per-field, you can also add an empty tag likejson:""orgolog:""to opt in.TaggedStructFields(s, "json")withjson:""now logs the field (Go field name), previously it skipped. This isencoding/json.Marshalparity.
omitnull vs omitzero, concretely: sql.NullString{Valid: false, String: ""} and sql.NullString{Valid: true, String: ""} are both the reflect zero value, but only the first is actually null. omitnull with a proper IsNull method distinguishes the two; omitzero cannot.
customLevels := &golog.Levels{
Trace: -20,
Debug: -10,
Info: 0,
Warn: 10,
Error: 20,
Fatal: 30,
Names: map[golog.Level]string{
-20: "TRACE",
-10: "DEBUG",
0: "INFO",
10: "WARN",
20: "ERROR",
30: "FATAL",
},
}Log levels have numeric values where lower = more verbose, higher = more severe:
Trace=-20 < Debug=-10 < Info=0 < Warn=10 < Error=20 < Fatal=30
// FilterOutBelow filters out less severe levels (lower numeric values)
// Warn=10, so this filters out Trace=-20, Debug=-10, Info=0
// Result: only Warn, Error, Fatal are logged
filter := golog.DefaultLevels.Warn.FilterOutBelow()
// Filter out only DEBUG level (Debug=-10)
debugFilter := golog.DefaultLevels.Debug.FilterOut()
// Filter out everything except INFO (Info=0)
infoOnlyFilter := golog.DefaultLevels.Info.FilterOutAllOther()
// Combine multiple filters (filters are OR'd together)
// This filters out both Trace and Debug
combinedFilter := golog.JoinLevelFilters(
golog.DefaultLevels.Trace.FilterOut(),
golog.DefaultLevels.Debug.FilterOut(),
)Set Format.Location to render every formatted time value, both the log line
timestamp and structured time.Time attributes, in a fixed timezone. The writer
calls time.Time.In on each value before formatting, so the underlying instant
is preserved. Leave it nil (the default) to keep each value's original location.
tokyo, _ := time.LoadLocation("Asia/Tokyo")
format := golog.NewDefaultFormat()
format.Location = tokyo
config := golog.NewConfig(
&golog.DefaultLevels,
golog.AllLevelsActive,
golog.NewJSONWriterConfig(os.Stdout, format),
)Use golog.Timestamp when you need to read log timestamps back out of JSON, a database column,
or any other place where the format varies. It wraps time.Time with JSON, sql.Scanner,
driver.Valuer, and null semantics, and parses a wide range of common layouts out of the box:
// JSON unmarshal: accepts RFC3339(+nano), golog's own
// "2006-01-02 15:04:05.000", Apache/NGINX CLF, RFC1123(Z),
// syslog Stamp, compact ISO 8601, Go log default, or a JSON
// number interpreted as Unix epoch seconds. JSON null is zero.
type logLine struct {
Time golog.Timestamp `json:"time"`
Level string `json:"level"`
Message string `json:"message"`
}
// Directly parse a string with any supported layout:
ts, err := golog.ParseTimestamp("2024-01-15 10:30:45.000")
// Plug into database/sql — SQL NULL maps to the zero value:
var ts golog.Timestamp
err = db.QueryRow("SELECT logged_at FROM events WHERE id = $1", id).Scan(&ts)
if ts.IsNull() {
// column was NULL
}The accepted layouts live in the exported golog.TimestampFormats slice, so you can extend
or replace it before use if your log source needs extra layouts. MarshalJSON always emits
DefaultTimeFormat (time.RFC3339Nano), or null when the value is zero.
For storing the current request's event time in context.Context, use
golog.ContextWithTimestamp(ctx, t) together with golog.TimestampFromContextOrNow(ctx)
(returns time.Now() when no timestamp is stored or the stored value is zero).
Upgrading from earlier versions: the old
golog.Timestamp(ctx context.Context) time.Timehelper has been renamed togolog.TimestampFromContextOrNow(ctx)to free theTimestampidentifier for the new type.ContextWithTimestampis now generic overtime.Time | golog.Timestamp.
golog is designed for high performance:
- Zero-allocation logging in most cases
- Object pooling for message and writer instances
- Efficient JSON encoding
- Lazy evaluation of expensive operations
- Minimal overhead for inactive log levels
Run internal benchmarks:
go test -bench=. -benchmemFor comparative benchmarks against other popular Go logging libraries (zerolog, zap, slog, logrus), the benchmarks are in a separate module to avoid adding those dependencies to the main golog module:
go test ./benchmarks -bench=. -benchmem -benchtime=1sSee benchmarks/README.md for detailed comparative analysis and performance insights.
golog is designed to strike a balance between performance and flexibility. While libraries like zerolog and zap prioritize raw speed, golog provides a richer feature set that makes it more adaptable to complex logging requirements.
| Feature | zerolog | zap | golog |
|---|---|---|---|
| Multi-writer support | Single output | Limited | Native, unlimited |
| Duplicate key prevention (inherited) | No | No | Yes |
| Context attribute integration | Manual | Manual | Automatic |
| Sub-logger with inherited attributes | Basic | Basic | Full support with attrib recording |
| Zero allocations (simple message) | Yes | Yes | Yes |
| Zero allocations (with fields) | Yes | No (1 alloc) | Yes |
| slog compatibility | Separate adapter | Separate adapter | Native goslog package |
zerolog: Extreme Minimalism
- Optimized for a single use case: fast JSON logging to a single output
- Minimal abstraction layers result in the fastest raw JSON performance
- Disabled log levels have near-zero overhead (~5 ns/op)
- Trade-off: Limited flexibility for complex logging scenarios
- Trade-off: ConsoleWriter for text output is significantly slower (2363 ns/op vs 51 ns/op for JSON)
zap: Performance + Type Safety
- Typed
Fieldstructs provide compile-time safety - Separate "Sugar" logger offers convenience at the cost of performance
- Trade-off: Allocates a slice for variadic field arguments (~1 alloc per log call with fields)
golog: Flexibility + Features
- Native multi-writer architecture: Log to console, files, and external services simultaneously with different formats and filters per destination
- Automatic context integration: Attributes added to
context.Contextare automatically included in log messages without manual plumbing - Sub-logger attribute recording: The
With().SubLogger()pattern creates child loggers that efficiently inherit and extend parent attributes - Duplicate key prevention: When a message sets a key already provided by the logger, a sub-logger, or the context, the inherited value is kept and the message-level duplicate is dropped, ensuring clean inherited structured data
- Zero allocations for standard logging: Despite the richer feature set, golog achieves zero allocations for JSON logging with fields
- Nil-safe design: A nil logger is safe to use and won't panic, simplifying error handling
golog is the right choice when you need:
- Multiple output destinations: Log to stdout with colors for development and JSON files for production simultaneously
- Request-scoped logging: Automatically propagate correlation IDs, user IDs, and other context through your application
- Sub-loggers with inherited context: Create child loggers for specific components that include parent attributes
- Clean structured data: Prevent keys inherited from sub-loggers or context from being duplicated by message-level attributes
- slog compatibility: Use golog as a backend for Go's standard library logging interface
- Rotating log files: Built-in support for size-based log rotation
- zerolog: When raw JSON logging speed is the only priority and you don't need multi-writer support, context integration, or text/console output (zerolog's ConsoleWriter is slow at 2363 ns/op)
- zap: When you prefer a variadic field API with compile-time type checking
- slog: When you want zero external dependencies and good-enough performance from the standard library
For most applications, the performance difference between logging libraries is negligible. At 250 ns/op for a simple JSON message, golog can handle 4 million log messages per second on a single core. With structured fields, golog (353 ns/op) is faster than zap (451 ns/op). With complex fields including error and time.Time, golog achieves 391 ns/op with zero allocations, outperforming zap's 475 ns/op with 1 allocation. The additional features golog provides—multi-writer support, context integration, and duplicate key prevention—often save more development time than the nanoseconds saved by faster alternatives.
Note on text output: golog is the fastest for text/console output at 394 ns/op with zero allocations. This outperforms zap (514 ns/op, 4 allocs), slog (677 ns/op, 3 allocs), and especially zerolog's ConsoleWriter (2363 ns/op with 46 allocations).
The performance gap for JSON output becomes meaningful only in extreme high-throughput scenarios (100K+ logs/second sustained), where zerolog's 51 ns/op provides measurable benefits. For typical applications, golog's flexibility and rich feature set make it a more productive choice.
- Logger: Main logging interface
- Message: Fluent message builder
- Config: Logger configuration
- WriterConfig: Output writer configuration
- Level: Log level type
- LevelFilter: Level filtering interface
- Format: Layout config for timestamp keys, timestamp layouts, level keys, message keys, structured
time.Timeattributes, and an optional*time.Locationto render every time value in a fixed timezone - Timestamp:
time.Timewrapper with JSON,database/sql.Scanner/driver.Valuer, and null semantics, tuned for parsing log timestamps in many common formats
- JSONWriter: Structured JSON output
- TextWriter: Human-readable text output
- CallbackWriter: Custom callback-based writer
- MultiWriter: Multiple writer composition
- NopWriter: No-operation writer for testing
For complete API documentation, see pkg.go.dev.
This project is licensed under the MIT License - see the LICENSE file for details.