golog

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 28, 2026 License: MIT Imports: 12 Imported by: 0

README

Golog

Structured logging for Go.

Go Reference

Use v2. New projects and documentation should target github.com/jkaveri/golog/v2. The module root import (github.com/jkaveri/golog without /v2) is the legacy v1 API; it remains published only for backward compatibility with existing code. Do not choose v1 for new work.

Table of Contents

v2 vs legacy at a glance

Topic v2 (github.com/jkaveri/golog/v2) Legacy v1 (github.com/jkaveri/golog)
Status Current API; new features land here Frozen; bugfixes / compatibility only
Shape Logger + slog-style Attr / Value, Config, Writer Package-level helpers + LogScope, LogWriter
Levels Debug, Info, Error only (no separate “warn”) Same three levels
Docs v2/README.md, pkg.go.dev This file (below), pkg.go.dev

Install and use the /v2 module:

go get github.com/jkaveri/golog/v2

Documentation

v2 provides a small Logger API (Debug, Info, Error, With, WithContext, WithError), slog-style Attr / Value, declarative Config (text or JSON, level, optional caller/source enrichment), pluggable Writer implementations (TextWriter, JSONWriter), and optional Enricher hooks. Package-level helpers (golog.Info, etc.) are available after optional InitDefault.

Preset configs are available via github.com/jkaveri/golog/v2/config (Development, Production, and related helpers).

v2 quick start
package main

import (
	"github.com/jkaveri/golog/v2"
)

func main() {
	log, err := golog.NewLogger(golog.Config{
		Format: golog.FormatText,
		Output: "", // stdout
		Level:  golog.LevelDebug,
	})
	if err != nil {
		panic(err)
	}

	log.Info("server started",
		golog.String("addr", ":8080"),
	)
}

Legacy API (v1)

Everything from Requirements onward in this file describes the legacy package at the module root (github.com/jkaveri/golog). That API is frozen for backward compatibility; refer to v2 for current behavior, types, and best practices.

Requirements

Features

v2 (see v2/README.md for detail):

  • Compact Logger interface with structured Attr fields
  • Text and JSON line writers, declarative Config, optional enrichers (context, source)
  • Level filtering on the logger; thread-safe writers

Legacy v1 (sections below):

  • Simple package-level API and scoped fields (With, WithContext, …)
  • Text and JSON LogWriter implementations
  • Global enrichers and level configuration

Installation (legacy v1)

For existing code that still uses the module root (v1):

go get github.com/jkaveri/golog

For new code, use v2 (github.com/jkaveri/golog/v2) instead.

Quick Start

package main

import "github.com/jkaveri/golog"

func main() {
    // Simple logging
    golog.Info("Application started")
    // Output: main.go:N [INFO][2026-03-30T12:34:56Z] Application started

    // Logging with fields
    golog.With("user_id", 123).
        With("username", "john_doe").
        Info("User logged in")
    // Output: main.go:N [INFO][2026-03-30T12:34:56Z] User logged in user_id="123" username="john_doe"

    // Logging with error
    err := someOperation()
    if err != nil {
        golog.WithError(err).Error("Operation failed")
        // Output: main.go:N [ERROR][2026-03-30T12:34:56Z] Operation failed error="operation failed: invalid input"
    }
}
JSON Logger

The JSON logger is suitable for production environments where logs need to be parsed by log aggregation tools.

package main

import (
    "os"
    "github.com/jkaveri/golog"
)

func main() {
    // Create a JSON logger writing to stdout
    jsonLogger := golog.NewJSONWriter(os.Stdout)

    // Set it as the default logger
    golog.SetWriter(jsonLogger)

    // Now all logs will be in JSON format
    golog.With("version", "1.0.0").
        With("environment", "production").
        Info("Application started")

    // Output will look like:
    // {"time":"2026-03-30T12:34:56Z","level":"INFO","msg":"Application started","version":"1.0.0","environment":"production","caller":"main.go:N"}\n
}

Interfaces

The library defines two main interfaces that can be implemented to customize logging behavior:

LogWriter Interface
type LogWriter interface {
    // Write writes a log entry with the given level, message, and fields
    Write(level int, msg string, fields map[string]any)
    // Flush ensures all buffered log entries are written
    Flush()
}

The LogWriter interface is responsible for the actual writing of log entries. The library provides two implementations:

  • defaultWriter: A text-based logger that writes human-readable logs
  • jsonWriter: A JSON logger that writes machine-readable logs
Enricher Interface
type Enricher interface {
    // Enrich adds additional fields to a log entry based on the context
    Enrich(ctx context.Context, level string, msg string, fields map[string]any)
}

The Enricher interface allows you to add additional context to log entries. Enrichers are called before each log entry is written and can modify the fields map to add more information.

Thread Safety

The LogScope type is not thread-safe and should not be shared between goroutines. Each goroutine should create its own scope using the provided factory functions (With, WithFields, WithContext, etc.). The underlying LogWriter implementations (defaultWriter and jsonWriter) are thread-safe and can be safely used from multiple goroutines.

Advanced Usage

Scoped Logging

Scoped logging allows you to create a context with predefined fields that will be included in all subsequent log messages.

func handleRequest(ctx context.Context, req *http.Request) {
    // Create a new logging scope with request context
    scope := golog.WithContext(ctx).
        With("request_id", req.Header.Get("X-Request-ID")).
        With("method", req.Method).
        With("path", req.URL.Path)

    // All logs in this scope will include the request fields
    scope.Info("Processing request")

    // Additional fields can be added to specific log lines
    scope.With("user_agent", req.UserAgent()).Debug("Request details")

    // Nested scopes inherit parent fields
    userScope := scope.With("user_id", 123)
    userScope.Info("User-specific operation")
}
Custom Writer Implementation

You can create your own writer implementation by implementing the LogWriter interface. This allows you to customize how logs are formatted and where they are written.

type CustomWriter struct {
    writer io.Writer
}

func NewCustomWriter(writer io.Writer) *CustomWriter {
    return &CustomWriter{writer: writer}
}

func (w *CustomWriter) Write(level int, msg string, fields map[string]any) {
    timestamp := time.Now().Format(time.RFC3339)
    levelStr := golog.LevelString(level)
    logLine := fmt.Sprintf("[%s] %s: %s", timestamp, levelStr, msg)

    for k, v := range fields {
        logLine += fmt.Sprintf(" %s=%v", k, v)
    }
    logLine += "\n"

    w.writer.Write([]byte(logLine))
}

func (w *CustomWriter) Flush() {
    // Implement flush logic if needed
}

// Usage
func main() {
    writer := NewCustomWriter(os.Stdout)
    golog.SetWriter(writer)

    golog.With("version", "1.0.0").Info("Application started")
}
Log Enrichment

You can enrich your logs with additional context using the Enricher interface and RegisterEnricher. Enrichers are applied globally to all log entries.

import (
    "context"
    "github.com/jkaveri/golog"
)

// Register at startup (e.g., in main or init)
func init() {
    golog.RegisterEnricher(golog.EnricherFunc(func(ctx context.Context, level, msg string, fields map[string]any) {
        if id := ctx.Value("request_id"); id != nil {
            fields["request_id"] = id
        }
    }))
}

// Usage
func handleRequest(w http.ResponseWriter, r *http.Request) {
    ctx := context.WithValue(r.Context(), "request_id", r.Header.Get("X-Request-ID"))
    golog.WithContext(ctx).Info("Processing request")
}
Error Handling

Golog provides convenient methods for error logging.

func processUser(userID string) error {
    user, err := fetchUser(userID)
    if err != nil {
        return golog.WithError(err).
            With("user_id", userID).
            With("operation", "fetch_user").
            Error("Failed to fetch user")
    }

    if err := validateUser(user); err != nil {
        return golog.WithError(err).
            With("user_id", userID).
            With("operation", "validate_user").
            Error("User validation failed")
    }

    return nil
}

Configuration

Log Levels

You can configure the minimum log level to control which messages are logged.

// Set minimum log level to Debug
golog.SetLevel(golog.LevelDebug)

// Set minimum log level to Info (default)
golog.SetLevel(golog.LevelInfo)

// Set minimum log level to Error
golog.SetLevel(golog.LevelError)

// Parse level from string (e.g., from environment variable)
if level := golog.ParseLevel(os.Getenv("LOG_LEVEL")); level >= 0 {
    golog.SetLevel(level)
}
Output Configuration
// Write to file
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
logger := golog.NewJSONWriter(file)
golog.SetWriter(logger)

// Write to multiple outputs
multiWriter := io.MultiWriter(os.Stdout, file)
logger := golog.NewJSONWriter(multiWriter)
golog.SetWriter(logger)

Unsupported Types

Golog uses bytedance/sonic for JSON encoding. As a result, the library inherits the same type limitations as sonic. When using structs with fields that contain unsupported types, you should use the json:"-" tag to skip those fields during logging. Any serialization issues, including unsupported types or invalid JSON structures, will cause a panic.

Unsupported types include:

  • Complex numbers (complex64, complex128)
  • Channels
  • Functions
  • Other types not supported by sonic

Example of handling unsupported types:

type User struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Complex   complex64 `json:"-"`  // Skip complex number field to avoid panic
    Channel   chan int  `json:"-"`  // Skip channel field to avoid panic
    Callback  func()    `json:"-"`  // Skip function field to avoid panic
}

func main() {
    user := User{
        ID:      "123",
        Name:    "John",
        Complex: 1 + 2i,
        Channel: make(chan int),
        Callback: func() {},
    }

    // This will work without panicking because unsupported fields are tagged with json:"-"
    golog.With().
        With("user", user).
        Info("User created")

    // This will panic because Complex field is not tagged with json:"-"
    golog.With().
        With("complex", 1+2i).
        Info("This will panic")
}

Best Practices

  1. Use Scoped Logging: Create scopes for different components or operations to maintain context.
  2. Include Relevant Fields: Always include fields that help with debugging and monitoring.
  3. Use Appropriate Log Levels:
    • Debug: Detailed information for debugging
    • Info: General operational information
    • Error: Error conditions that need attention
  4. Structured Data: Use the field system instead of string interpolation for better parsing.
  5. Context Preservation: Use WithContext when working with request-scoped operations.
  6. Type Limitations: Be aware that certain Go types are not supported in fields and will cause a panic:
    • Complex numbers (complex64, complex128)
    • Channels
    • Other unsupported types will cause a panic

API Reference

The following applies to the legacy v1 package (github.com/jkaveri/golog) only. For v2, see pkg.go.dev/github.com/jkaveri/golog/v2.

Functions
  • golog.Debug(msg string, args ...any) - Log a debug message; args are passed to fmt.Sprintf
  • golog.Info(msg string, args ...any) - Log an info message; args are passed to fmt.Sprintf
  • golog.Error(msg string, args ...any) error - Log an error message and return an error for propagation
  • golog.With(key string, value any) *LogScope - Create a new scope with a field
  • golog.WithFields(fields map[string]any) *LogScope - Create a new scope with multiple fields
  • golog.WithPairs(args ...any) *LogScope - Create a scope from alternating key-value pairs (panics if odd length or non-string keys)
  • golog.WithContext(ctx context.Context) *LogScope - Create a new scope with context
  • golog.WithError(err error) *LogScope - Create a new scope with error
  • golog.RegisterEnricher(enricher Enricher) - Register a global enricher
  • golog.SetWriter(writer LogWriter) - Set a custom logger implementation
  • golog.SetLevel(level int) - Set the minimum log level (use LevelDebug, LevelInfo, LevelError)
  • golog.ParseLevel(level string) int - Parse level from string; returns -1 if invalid
  • golog.LevelString(level int) string - Convert level int to string
  • golog.Flush() - Flush any buffered logs
Types
  • golog.LogWriter - Interface for logger implementations
  • golog.LogScope - Represents a logging scope with propagated fields (not thread-safe)
  • golog.Enricher - Interface for log enrichment
  • golog.EnricherFunc - Function type implementing Enricher
Level Constants
  • golog.LevelDebug (0), golog.LevelInfo (1), golog.LevelError (2)
Implementations
  • golog.NewDefaultWriter(writer io.Writer) *defaultWriter - Creates a default text-based logger
  • golog.NewJSONWriter(writer io.Writer) *jsonWriter - Creates a JSON logger
Common Pitfalls
  • Unsupported types: Complex numbers, channels, and functions in fields cause a panic. Use json:"-" on struct fields or avoid these types.
  • WithPairs: Must have an even number of arguments; keys must be strings. Panics otherwise.
  • Thread safety: LogScope is not thread-safe; create a new scope per goroutine.

Contributing

Contributions are welcome. Please open an issue to discuss larger changes, and ensure go test ./... passes from the repository root (including v2/) before submitting a pull request.

License

This project is licensed under the MIT License; see LICENSE.

Documentation

Overview

Package golog provides a simple, structured logging library for Go applications. It supports JSON output, context-based logging, and field enrichment.

Architecture

Golog uses three main concepts:

  • Writers: Implement LogWriter to control where and how logs are formatted (e.g., JSON, human-readable).
  • Scopes: LogScope holds fields and context; create scopes with With, WithFields, WithContext, WithError, or WithPairs.
  • Enrichers: Implement Enricher to add fields to log entries; register globally with RegisterEnricher.

Features

  • Structured JSON or text logging
  • Context support for request-scoped fields
  • Field enrichment via Enricher
  • Multiple log levels (Debug, Info, Error)
  • Flushable output

Thread Safety

LogScope is not safe for concurrent use; create a new scope per goroutine or operation. LogWriter implementations (NewDefaultWriter, NewJSONWriter) are thread-safe.

Example

writer := golog.NewJSONWriter(os.Stdout)
golog.SetWriter(writer)

golog.Info("Hello, world!")
golog.With("user_id", 123).Info("User logged in")
golog.WithError(err).Error("Operation failed")
golog.WithContext(ctx).WithFields(map[string]any{"request_id": "abc"}).Info("Request processed")
golog.WithPairs("user_id", 123, "action", "login").Info("User logged in")
golog.SetLevel(golog.LevelDebug)
golog.SetSkipFrames(2)

Index

Examples

Constants

View Source
const (
	LevelDebug = iota // 0 - detailed debugging information
	LevelInfo         // 1 - general operational information (default minimum)
	LevelError        // 2 - error conditions
)

Log level constants. Lower values are less severe; only messages with level >= the minimum (set via SetLevel) are logged.

View Source
const (
	// FieldTime is the key for time of log
	FieldTime = "time"
	// FieldLevel is the key for level of log
	FieldLevel = "level"
	// FieldMessage is the key for message of log
	FieldMessage = "msg"

	// FieldCaller is the key for caller of log
	FieldCaller = "caller"
)

Standard JSON log field names used in log output

Variables

This section is empty.

Functions

func Debug

func Debug(msg string, args ...any)

Debug logs a message at the debug level. Args are passed to fmt.Sprintf for message formatting.

func Error

func Error(msg string, args ...any) error

Error logs a message at the error level and returns an error for propagation. Args are passed to fmt.Sprintf for message formatting.

func Flush

func Flush()

Flush ensures all buffered log entries are written. It calls Flush on the global log writer instance.

func GetSkipFrames added in v1.1.0

func GetSkipFrames() int

GetSkipFrames returns the number of frames to skip when logging.

func Info

func Info(msg string, args ...any)

Info logs a message at the info level. Args are passed to fmt.Sprintf for message formatting.

func LevelString

func LevelString(level int) string

LevelString converts an integer level to its string representation. Returns "UNKNOWN" if the level is invalid.

func NewDefaultWriter

func NewDefaultWriter(output io.Writer) *defaultWriter

NewDefaultWriter creates a new defaultWriter instance with the given io.Writer. The writer is wrapped in a buffer for better performance. Unsupported field types (complex64, complex128, channels, functions) will cause a panic.

Example:

writer := NewDefaultWriter(os.Stdout)

func NewJSONWriter

func NewJSONWriter(output io.Writer) *jsonWriter

NewJSONWriter creates a new JSON logger that writes machine-readable logs to the given io.Writer. Each log entry is a single JSON object with keys: time, level, msg, caller, plus any custom fields. Ideal for production environments and log aggregation tools (e.g., ELK, Datadog).

Example output:

{"time":"2024-03-30T12:34:56Z","level":"INFO","msg":"User logged in","caller":"main.go:42","user_id":123}

func ParseLevel

func ParseLevel(level string) int

ParseLevel converts a string level name to its integer value. The parsing is case-insensitive (e.g., "debug", "DEBUG", "Debug" all map to LevelDebug). Returns -1 if the level name is invalid.

Example
fmt.Println(ParseLevel("debug"))
fmt.Println(ParseLevel("INFO"))
fmt.Println(ParseLevel("invalid"))
Output:
0
1
-1

func RegisterEnricher

func RegisterEnricher(enricher Enricher)

RegisterEnricher adds a new enricher to the global enrichers list. Enrichers are called in the order they are registered.

func SetLevel

func SetLevel(level int)

SetLevel sets the minimum log level that should be logged. Only messages with severity >= minLevel will be logged. Use LevelDebug, LevelInfo, or LevelError, or ParseLevel for string-based config.

func SetSkipFrames

func SetSkipFrames(skip int)

SetSkipFrames sets the number of frames to skip when logging. This is useful for logging from functions that are called by other functions.

func SetWriter

func SetWriter(logger LogWriter)

SetWriter sets the global log writer instance. This function should be called at application startup to configure logging.

Types

type Enricher

type Enricher interface {
	// Enrich adds additional fields to a log entry based on the context.
	// Modify the fields map in place; do not replace it.
	Enrich(ctx context.Context, level string, msg string, fields map[string]any)
}

Enricher defines the interface for log entry enrichment. Enrichers add additional fields to log entries based on context. The fields map is modified in place; add keys to enrich the log entry.

type EnricherFunc

type EnricherFunc func(ctx context.Context, level string, msg string, fields map[string]any)

EnricherFunc is a function type that implements the Enricher interface. Use it to create enrichers without defining a new type:

golog.RegisterEnricher(golog.EnricherFunc(func(ctx context.Context, level, msg string, fields map[string]any) {
    fields["trace_id"] = traceIDFromContext(ctx)
}))
Example
// EnricherFunc allows using a function as an Enricher without defining a new type
enricher := EnricherFunc(func(ctx context.Context, level, msg string, fields map[string]any) {
	fields["trace_id"] = "abc-123"
})
// Use with RegisterEnricher at startup
_ = enricher
fmt.Println("EnricherFunc registered")
Output:
EnricherFunc registered

func (EnricherFunc) Enrich

func (f EnricherFunc) Enrich(ctx context.Context, level string, msg string, fields map[string]any)

Enrich implements the Enricher interface for EnricherFunc. It simply calls the underlying function with the provided arguments.

type LogScope

type LogScope struct {
	// contains filtered or unexported fields
}

LogScope represents a logging context with associated fields and enrichers. It supports method chaining (With, WithFields, WithContext, WithError) and is typically used for request handlers or operations where fields should propagate to all log calls. LogScope is not thread-safe; create a new scope per goroutine.

func With

func With(key string, value any) *LogScope

With creates a new LogScope with a single key-value field. LogScope is not thread-safe; create a new scope per goroutine.

func WithContext

func WithContext(ctx context.Context) *LogScope

WithContext creates a new LogScope with the given context. It is a convenience function for creating a scope with a context.

func WithError

func WithError(err error) *LogScope

WithError creates a new LogScope with an error field. It is a convenience function for creating a scope with an error.

func WithFields

func WithFields(fields map[string]any) *LogScope

WithFields creates a new LogScope with multiple fields. It is a convenience function for creating a scope with multiple fields at once.

func WithPairs

func WithPairs(args ...any) *LogScope

WithPairs creates a new LogScope with multiple fields from alternating key-value pairs. Args must be an even number: key1, value1, key2, value2, ... Panics if args has odd length or if any key is not a string.

Example
buf := &bytes.Buffer{}
oldWriter := instance
instance = NewDefaultWriter(buf)
defer func() { instance = oldWriter }()

WithPairs("user_id", 123, "action", "login").Info("User logged in")
instance.Flush()

output := buf.String()
if strings.Contains(output, "User logged in") && strings.Contains(output, "user_id") {
	fmt.Println("ok")
}
Output:
ok

func (*LogScope) Context

func (l *LogScope) Context() context.Context

Context returns the context associated with this LogScope.

func (*LogScope) Debug

func (l *LogScope) Debug(msg string, args ...any)

Debug writes a log entry at the debug level. The message and any additional arguments are formatted using fmt.Sprintf.

func (*LogScope) Error

func (l *LogScope) Error(msg string, args ...any) error

Error writes a log entry at the error level and returns an error for propagation. The message and any additional arguments are formatted using fmt.Sprintf. If the scope has an error field (via WithError), returns errors.Wrap of that error; otherwise returns a new error with the formatted message.

func (*LogScope) Flush

func (l *LogScope) Flush()

Flush ensures all buffered log entries are written. It calls Flush on the underlying log writer.

func (*LogScope) Info

func (l *LogScope) Info(msg string, args ...any)

Info writes a log entry at the info level. The message and any additional arguments are formatted using fmt.Sprintf.

func (*LogScope) With

func (l *LogScope) With(key string, value any) *LogScope

With adds a key-value field to this LogScope. It returns the LogScope for method chaining.

func (*LogScope) WithContext

func (l *LogScope) WithContext(ctx context.Context) *LogScope

WithContext sets the context for this LogScope. It returns the LogScope for method chaining.

func (*LogScope) WithError

func (l *LogScope) WithError(err error) *LogScope

WithError adds an error field to this LogScope. It returns the LogScope for method chaining.

func (*LogScope) WithFields

func (l *LogScope) WithFields(fields map[string]any) *LogScope

WithFields adds multiple key-value fields to this LogScope. It returns the LogScope for method chaining.

type LogWriter

type LogWriter interface {
	// Write writes a log entry with the given level, message, and fields
	Write(level int, msg string, fields map[string]any)
	// Flush ensures all buffered log entries are written
	Flush()
}

LogWriter defines the interface for log output writers. Implementations should handle the actual writing of log entries.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL