log

package module
v1.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 10 Imported by: 37

README

go-log 🪵

🧵 Tiny structured logging for Go services on zerolog, with OpenTelemetry trace_id/span_id correlation baked in.

📦 Install

go get github.com/Bugs5382/go-log

🚀 Usage

logger := log.New("my-service")
logger.Info().Msg("started")

// Inside a request/span, correlate logs with the active trace:
l := log.Ctx(ctx)
l.Info().Msg("handling request")

New writes JSON to stdout with a timestamp and a service field. Ctx derives a logger that adds trace_id and span_id from the active OpenTelemetry span, pairing with go-otel. 🔗

⚠️ Ctx is deprecated. Having no receiver, it derives from whichever logger New created last, so a process with two loggers cannot get the right service from both. Prefer NewLogger and the Logger.Ctx method below, which derives from the logger it is called on.

Neutral Logger (no zerolog dependency)

New and Ctx above return a concrete zerolog.Logger, so a wrapper package that holds one is forced to import github.com/rs/zerolog just to type the variable. NewLogger and LoggerFromContext return the neutral Logger interface instead: no zerolog type appears in any of its method signatures, so a consumer can depend on it (or an interface shaped like it) without ever importing zerolog. zerolog stays an internal implementation detail behind this path -- LOG_LEVEL, LOG_FORMAT, and OpenTelemetry trace correlation all behave exactly as they do for New/Ctx.

var logger log.Logger = log.NewLogger("my-service")
logger.Info("started", log.F("port", 8080))

child := logger.With(log.F("request_id", reqID))
child.Warn("slow downstream call", log.F("elapsed_ms", 420))
child.Error(err, "request failed")

// Inside a request/span, correlate logs with the active trace:
l := logger.Ctx(ctx)
l.Info("handling request")

Logger covers Debug/Info/Warn/Error/Fatal with structured Fields (build one with log.F(key, val)), a With(fields...) Logger for child loggers, and a Ctx(ctx) Logger method. Logger.Ctx derives from the receiver, so the service name and any fields already added with With are carried onto the correlated logger.

The package-level Ctx and LoggerFromContext are deprecated in favour of Logger.Ctx -- both read the logger most recently built by New, which is ambiguous once a process has more than one.

Options (output, default level and format)

NewLoggerWithOptions builds the same neutral Logger as NewLogger, tuned by options. With no options it behaves exactly like NewLogger.

level := log.LevelInfo
if verbose {
	level = log.LevelTrace
}

logger := log.NewLoggerWithOptions("my-cli",
	log.WithOutput(os.Stderr),                // logs never mix with stdout
	log.WithDefaultFormat(log.FormatConsole), // plain text unless LOG_FORMAT says otherwise
	log.WithDefaultLevel(level),              // used only when LOG_LEVEL is unset or invalid
)
logger.Trace("resolved config", log.F("path", path))
  • WithOutput(w) sends every line to w instead of stdout, in any format. With LOG_FORMAT=both the JSON half goes to w and the console half still goes to stderr.
  • WithDefaultLevel(level) replaces info as the fallback level. 🎛 The environment still wins: a valid LOG_LEVEL always overrides it. Level is a string type using the LOG_LEVEL names (log.LevelTrace ... log.LevelDisabled), so log.Level(flagValue) works too.
  • WithDefaultFormat(format) replaces JSON as the fallback format (log.FormatJSON, log.FormatConsole, log.FormatBoth). A valid LOG_FORMAT always overrides it.
Trace and the no-op Logger

NewLoggerWithOptions and Nop return a TraceLogger: a Logger plus a Trace(msg, fields...) method. Every logger this package hands out (children from With and Ctx too) has Trace; for a value typed as Logger, call log.Trace(logger, msg, fields...). Trace lives on its own interface because adding a method to Logger would break anyone who implements it.

log.Nop() writes nothing, whatever LOG_LEVEL says, which suits tests and quiet modes. Its Fatal still exits the process, as callers expect.

Live level changes

The level is live and process-wide. Every logger is built at trace and filtered by one atomic level that zerolog reads on each event, so a change reaches the loggers a service already holds, their With/Ctx children and any package-level logger, with no restart and no rebuild. LOG_LEVEL (or the WithDefaultLevel fallback) is the start value.

logger := log.NewLogger("my-service")

// Follow the level in the file named by LOG_LEVEL_FILE, if it is set:
log.WatchLevelFileFromEnv(ctx)

// Or name the file directly, or set the level from code:
log.WatchLevelFile(ctx, "/etc/my-service/log-level")
_ = log.SetLevel(log.LevelDebug)
fmt.Println(log.CurrentLevel()) // debug
  • WatchLevelFile(ctx, path) reads a one-word level file (trace, debug, info, warn, error) before it returns, then re-reads it every two seconds until ctx ends. It follows symlinks on every read, so a Kubernetes ConfigMap volume works as the source: edit the ConfigMap and the pods pick it up once the kubelet syncs the volume (up to about a minute).
  • 🚦 Bad input never silences a service: an unknown word keeps the current level and logs a warning. A missing, unreadable or empty file means the start level from LOG_LEVEL. The watcher's own lines (a change, a bad word, a missing file) are written whatever the level is, unless it is disabled.
  • SetLevel(level) returns an error for an unknown name and leaves the level alone. Once a live level is set, loggers built later keep it.
  • One level per process: the live level is zerolog's global level, so it also filters any other zerolog logger in the process.

🛠 Develop

task build    # go build ./...
task test     # go test ./...
task lint     # gofmt check + golangci-lint + yamllint
task license  # inject MIT headers (golic)

⚖️ License

MIT © 2026 Shane

Documentation

Overview

Package log provides zerolog-based structured logging for Go services, with OpenTelemetry trace_id/span_id correlation drawn from the active span context.

New and Ctx return a concrete zerolog.Logger for callers that already depend on zerolog. NewLogger and LoggerFromContext return the neutral Logger interface instead: no zerolog type appears in its method signatures, so a consumer can wrap or depend on it without importing zerolog directly. Both paths honor the same LOG_LEVEL/LOG_FORMAT environment controls and the same trace correlation; zerolog stays an internal implementation detail behind the neutral path.

NewLoggerWithOptions builds a neutral Logger tuned by options: WithOutput picks the io.Writer (a CLI uses os.Stderr), and WithDefaultLevel and WithDefaultFormat set the fallbacks used when LOG_LEVEL or LOG_FORMAT is unset or invalid. It returns a TraceLogger, which adds Trace; Nop returns a TraceLogger that writes nothing.

LOG_LEVEL sets the start level. SetLevel changes the level live for the whole process, reaching loggers that already exist, and WatchLevelFile (or WatchLevelFileFromEnv with LOG_LEVEL_FILE) follows a one-word level file such as a mounted Kubernetes ConfigMap key.

Index

Examples

Constants

View Source
const LevelFileEnv = "LOG_LEVEL_FILE"

LevelFileEnv names the environment variable WatchLevelFileFromEnv reads: the path of a file holding one level word.

Variables

This section is empty.

Functions

func Ctx deprecated

func Ctx(ctx context.Context) zerolog.Logger

Ctx returns a zerolog.Logger derived from the logger created by the most recent call to New, with the active span's trace_id and span_id attached when ctx carries a valid span. This lets every log line be correlated with its trace in the tracing backend. When no valid span is present, it returns the plain base logger. If New has not been called, it falls back to a logger with no service field.

Deprecated: with two or more loggers in one process this returns whichever was created last, which is rarely what the caller means. Use NewLogger and the Logger.Ctx method, which derives from the logger it is called on.

func New

func New(service string) zerolog.Logger

New returns a zerolog.Logger with a timestamp and the given service name attached to every line, filtered to the level from LOG_LEVEL (default info) and rendered per LOG_FORMAT (JSON by default, console/pretty for dev). Consumers no longer need to parse LOG_LEVEL or wire a writer themselves -- calling New is enough.

func SetLevel added in v1.4.0

func SetLevel(l Level) error

SetLevel changes the minimum level of every logger in the process, live: no logger needs rebuilding. It accepts the LOG_LEVEL names (trace, debug, info, warn, error, fatal, panic, disabled), case-insensitively. An unknown name returns an error and leaves the level as it was.

LOG_LEVEL stays the start value; SetLevel replaces it until the process ends, and loggers built afterwards keep the level SetLevel chose.

func Trace added in v1.3.0

func Trace(l Logger, msg string, fields ...Field)

Trace logs msg at trace level through l when l is a TraceLogger, as every Logger from this package is. For a Logger without a Trace method the line is dropped, since trace is below every level such a Logger can write.

func WatchLevelFile added in v1.4.0

func WatchLevelFile(ctx context.Context, path string)

WatchLevelFile applies the level named in the file at path, then keeps applying it whenever the file changes, until ctx ends. The file holds one level word (trace, debug, info, warn or error; surrounding whitespace is ignored). It is read once before WatchLevelFile returns and then every two seconds in the background, following symlinks on each read, so a Kubernetes ConfigMap volume (which swaps a symlink when the ConfigMap changes) works as the source.

An unknown word keeps the current level and logs a warning. A missing, unreadable or empty file means the start level from LOG_LEVEL. The watcher's own lines (a level change, a bad word, a missing file) are written whatever the level is, since they describe the level itself; only disabled silences them. They go to the logger built most recently by this package.

func WatchLevelFileFromEnv added in v1.4.0

func WatchLevelFileFromEnv(ctx context.Context) bool

WatchLevelFileFromEnv calls WatchLevelFile with the path in LOG_LEVEL_FILE and reports whether the variable was set. With it unset, nothing is watched and LOG_LEVEL alone sets the level.

Types

type Field added in v1.2.0

type Field struct {
	Key string
	Val any
}

Field is a structured key/value pair attached to a Logger call. Build one with F, or construct it directly since both fields are exported.

func F added in v1.2.0

func F(key string, val any) Field

F builds a Field for a neutral Logger call, e.g. log.F("order_id", id).

type Format added in v1.3.0

type Format string

Format names an output format for WithDefaultFormat. The values match the LOG_FORMAT vocabulary. An empty or unrecognized Format means JSON.

const (
	// FormatJSON writes structured JSON. It is the default.
	FormatJSON Format = "json"
	// FormatConsole writes human-readable, colorized text.
	FormatConsole Format = "console"
	// FormatBoth writes JSON to the output and a console rendering to stderr.
	FormatBoth Format = "both"
)

The formats WithDefaultFormat accepts.

type Level added in v1.3.0

type Level string

Level names a minimum log level for WithDefaultLevel. The values match the LOG_LEVEL vocabulary, so a string from a flag or config file converts with Level(s). An empty or unrecognized Level means info.

const (
	LevelTrace    Level = "trace"
	LevelDebug    Level = "debug"
	LevelInfo     Level = "info"
	LevelWarn     Level = "warn"
	LevelError    Level = "error"
	LevelFatal    Level = "fatal"
	LevelPanic    Level = "panic"
	LevelDisabled Level = "disabled"
)

The levels WithDefaultLevel accepts, from most to least verbose.

func CurrentLevel added in v1.4.0

func CurrentLevel() Level

CurrentLevel returns the live minimum level.

type Logger added in v1.2.0

type Logger interface {
	// Debug logs msg at debug level with the given structured fields.
	Debug(msg string, fields ...Field)
	// Info logs msg at info level with the given structured fields.
	Info(msg string, fields ...Field)
	// Warn logs msg at warn level with the given structured fields.
	Warn(msg string, fields ...Field)
	// Error logs msg at error level, attaching err, with the given
	// structured fields.
	Error(err error, msg string, fields ...Field)
	// Fatal logs msg at fatal level, attaching err, with the given
	// structured fields, then terminates the process with a non-zero exit
	// code.
	Fatal(err error, msg string, fields ...Field)
	// With returns a child Logger that carries fields on every subsequent
	// line, in addition to whatever the receiver already carries.
	With(fields ...Field) Logger
	// Ctx returns a Logger correlated with the trace/span carried by ctx.
	// It derives from the receiver, so the service name and any fields
	// already attached via With are carried over. When ctx has no valid
	// span it returns the receiver unchanged.
	Ctx(ctx context.Context) Logger
}

Logger is the neutral logging surface: no zerolog type appears in any method signature here, so a consumer can depend on this interface alone and never import zerolog directly. zerolog remains an internal implementation detail behind it. New/Ctx (which do return a concrete zerolog.Logger) are unchanged and continue to work side by side with this interface.

Example

ExampleLogger shows the neutral surface: a consumer only needs the Logger interface, F, NewLogger, and LoggerFromContext -- zerolog is never imported or referenced.

package main

import (
	"context"
	"errors"

	log "github.com/Bugs5382/go-log"
)

func main() {
	logger := log.NewLogger("billing")

	logger.Info("service started", log.F("port", 8080))

	child := logger.With(log.F("request_id", "r-123"))
	child.Warn("slow downstream call", log.F("elapsed_ms", 420))

	err := errors.New("payment gateway timeout")
	child.Error(err, "request failed")

	// Inside a request/span, correlate logs with the active trace without
	// ever touching a zerolog type.
	ctx := context.Background()
	log.LoggerFromContext(ctx).Info("handling request")
}

func LoggerFromContext deprecated added in v1.2.0

func LoggerFromContext(ctx context.Context) Logger

LoggerFromContext returns a neutral Logger correlated with the trace/span carried by ctx -- the neutral equivalent of Ctx.

Deprecated: like Ctx, this derives from the logger created by the most recent call to New rather than from a logger the caller holds. Build a Logger with NewLogger and use its Ctx method instead.

func NewLogger added in v1.2.0

func NewLogger(service string) Logger

NewLogger returns a neutral Logger for a service, honoring LOG_LEVEL and LOG_FORMAT exactly like New. Use this when the caller must not import zerolog directly; use New when a concrete zerolog.Logger is wanted instead.

type Option added in v1.3.0

type Option func(*config)

Option configures a Logger built by NewLoggerWithOptions.

func WithDefaultFormat added in v1.3.0

func WithDefaultFormat(f Format) Option

WithDefaultFormat sets the format used when LOG_FORMAT is unset or unrecognized, in place of JSON. LOG_FORMAT still wins whenever it is valid. A CLI passes FormatConsole so people read plain text unless they ask for JSON with LOG_FORMAT=json.

func WithDefaultLevel added in v1.3.0

func WithDefaultLevel(l Level) Option

WithDefaultLevel sets the level used when LOG_LEVEL is unset or not a level name, in place of info. LOG_LEVEL still wins whenever it is valid, so a deployment can always override the code. A CLI maps its -v flag here.

func WithOutput added in v1.3.0

func WithOutput(w io.Writer) Option

WithOutput sends log output to w instead of stdout, whatever the format. A CLI passes os.Stderr so its logs never mix with what it prints on stdout. With LOG_FORMAT=both the JSON half goes to w and the console half still goes to stderr. A nil w leaves the output on stdout.

type TraceLogger added in v1.3.0

type TraceLogger interface {
	Logger
	// Trace logs msg at trace level with the given structured fields.
	Trace(msg string, fields ...Field)
}

TraceLogger is a Logger that can also log at trace level. It is a separate interface because adding a method to Logger would break every type outside this package that implements it.

Every Logger this package returns is a TraceLogger, including the children from With and Ctx. NewLoggerWithOptions and Nop return one directly; for a value typed as Logger, use the Trace function.

func NewLoggerWithOptions added in v1.3.0

func NewLoggerWithOptions(service string, opts ...Option) TraceLogger

NewLoggerWithOptions returns a neutral Logger for a service, like NewLogger, configured by opts. With no options it behaves exactly like NewLogger. The environment still has the last word: LOG_LEVEL and LOG_FORMAT override the defaults the options set whenever they hold a valid value.

The result is a TraceLogger, so Trace can be called on it directly. Like NewLogger, it also becomes the logger the package-level Ctx and LoggerFromContext derive from.

Example

ExampleNewLoggerWithOptions shows a CLI setup: logs on stderr so they never mix with command output, console text unless LOG_FORMAT asks for JSON, and a -v flag that selects trace unless LOG_LEVEL says otherwise.

package main

import (
	"os"

	log "github.com/Bugs5382/go-log"
)

func main() {
	verbose := true

	level := log.LevelInfo
	if verbose {
		level = log.LevelTrace
	}

	logger := log.NewLoggerWithOptions("my-cli",
		log.WithOutput(os.Stderr),
		log.WithDefaultFormat(log.FormatConsole),
		log.WithDefaultLevel(level),
	)

	logger.Trace("resolved config", log.F("path", "config.yaml"))
	logger.Info("done", log.F("files", 12))
}

func Nop added in v1.3.0

func Nop() TraceLogger

Nop returns a TraceLogger that writes nothing, for tests and quiet modes. It ignores LOG_LEVEL and LOG_FORMAT. Fatal still ends the process with a non-zero exit code, since callers rely on it not returning.

Example

ExampleNop shows the no-op Logger for tests and quiet modes.

package main

import (
	log "github.com/Bugs5382/go-log"
)

func main() {
	var logger log.Logger = log.Nop()
	logger.Info("never written")
	log.Trace(logger, "never written either")
}

Jump to

Keyboard shortcuts

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