Help us shape the future of Go! Take the 2026 Go Developer Survey

zslogfx

package module
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 14 Imported by: 0

README

CI Go Reference

zslogfx

A JSON log/slog logger backed by zap, with global slog.Default installation and Uber Fx lifecycle management.

Installation

go get github.com/uchaloop/zslogfx

Fx

fx.New(
	confx.Module(),
	confx.Provide[zslogfx.Config]("log"),
	zslogfx.Module(),
).Run()

Config names its default instance log, which gives the prefix, so it reads LOG_LEVEL and the rest of LOG_*:

LOG_LEVEL=info
LOG_CALLER_ENABLED=false

After installation, use the standard package-level API anywhere:

slog.Info("service started", "port", 8080)
slog.Error("request failed", "error", err)

The same *slog.Logger is also available through Fx dependency injection.

Fx logs through it too: its own events at debug, which LOG_LEVEL=info keeps out of the way, and the failures of a provide, a hook or a rollback at error. Module goes at the fx.New root, and one application at a time owns slog.Default.

A Config assembled in Go works just as well, for a tool that reads no environment at all:

fx.New(
	fx.Supply(zslogfx.Config{Level: "info"}),
	zslogfx.Module(),
)

Configuration

Under the log instance name:

Variable Type When unset
LOG_LEVEL string (debug, info, warn/warning, error) info
LOG_CALLER_ENABLED *bool on at debug level, off otherwise

An explicitly empty LOG_CALLER_ENABLED is invalid; set true or false, or leave it unset for automatic behavior.

Nothing is required: a logger with no configuration at all is a working logger. confmaker.Manifest[zslogfx.Config]("log") lists the same set from the type itself.

Fields and options

Add fields resolved from the Fx container:

zslogfx.AsOptions(
	func(build BuildInfo) zslogfx.Option {
		return zslogfx.WithFields(
			slog.String("service.version", build.Version),
		)
	},
)

Options that need nothing from the graph go through SupplyOptions. A plain fx.Supply does not reach the module, which reads a value group:

zslogfx.SupplyOptions(
	zslogfx.WithFields(
		slog.String("service.name", "orders"),
	),
)

The Options of one SupplyOptions call are applied in the order given. A value group has none, so the order between separate AsOptions and SupplyOptions calls is undefined: do not contribute two Options that set the same thing, such as two WithLevel or two WithWriteSyncer.

The options:

Option Effect
WithLevel overrides Config.Level
WithCaller overrides Config.Caller.Enabled
WithStacktraceLevel adds error.stack_trace from that level up; off unless set
WithFields attributes added to every record
WithWriteSyncer a destination other than stdout

Without Fx:

logger, err := zslogfx.Make(cfg, opts...)
if err != nil {
	return err
}
defer logger.Sync()

logger.Slog.Info("service started")

Output

Records are JSON with fields suitable for OpenSearch:

@timestamp            always
log.level             always
message               always
caller                with caller annotations
error.message         with an error attribute
error.stack_trace     with WithStacktraceLevel

A top-level error attribute is written as error.message with the error text, because ECS makes error an object and a scalar error would conflict with error.stack_trace. This covers the errors Fx logs itself. An error group is written as an object, and an error attribute inside a group keeps its key.

Timestamps are RFC 3339 with nanoseconds. Durations are encoded as nanoseconds, the unit ECS gives event.duration, so name a field of your own without a unit suffix. Values implementing slog.LogValuer, including secret.Secret, control their own representation.

The library adds no write buffer: every logging call hands the record to the destination synchronously, so a crash does not take a queue of records with it. Sync flushes what the destination itself buffers, may be called as often as the application likes, and leaves the logger usable. The logger serializes writes, so a destination given to WithWriteSyncer does not have to be safe for concurrent use.

OpenSearch mapping

Dynamic mapping reads these records, but it gives the stack trace a keyword sub-field it will never use, and it rounds @timestamp to milliseconds. An index template settles both:

{
  "mappings": {
    "properties": {
      "@timestamp": { "type": "date_nanos" },
      "log": { "properties": { "level": { "type": "keyword" } } },
      "message": { "type": "text" },
      "caller": { "type": "keyword" },
      "error": {
        "properties": {
          "message": { "type": "text" },
          "stack_trace": { "type": "text" }
        }
      }
    }
  }
}

A date field parses the timestamps as well, it just keeps milliseconds of them. Where the cluster offers them, match_only_text for message and error.message and wildcard for error.stack_trace index the same content for less.

Acknowledgements

I am grateful to the authors of zap, its zapslog bridge, and Uber Fx. Their work made this library possible.

License

MIT

Documentation

Overview

Package zslogfx builds a standard log/slog logger backed by zap and wires its ownership into an Uber Fx application.

Services use a plain *slog.Logger; zap is only the fast JSON backend, through Uber's official go.uber.org/zap/exp/zapslog handler. The output is one fixed JSON format, ECS-aligned for OpenSearch. Module installs the logger as slog.Default so any code can log without receiving it as an argument, and also provides it for explicit injection. The library adds no write buffer: every logging call hands the record to the destination synchronously.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AsOptions

func AsOptions(ctors ...any) fx.Option

AsOptions provides Option constructors to Module through an Fx value group. Constructors may depend on any other values in the Fx graph.

A value group has no order, so Options from separate AsOptions and SupplyOptions calls are applied in an undefined one: an application must not contribute two Options that set the same thing, such as two WithLevel or two WithWriteSyncer.

func Module

func Module() fx.Option

Module installs one application logger as slog.Default, provides *Logger and *slog.Logger, routes Fx events through it, and on application shutdown syncs it and restores the previous slog.Default. Fx events are logged at debug level, the failures among them at error level, in the same JSON as the rest.

The logger is constructed before any application component: both the Fx event logger and an eager fx.Invoke depend on it, so slog.Default is installed during fx.New - before OnStart hooks and application code run. This makes the logging sequence deterministic: no component logs before the logger is ready.

Module must be passed at the fx.New root because fx.WithLogger is scoped to the scope it is declared in. It belongs to one application at a time: two applications alive at once take slog.Default from each other, and an application that replaces the Fx event logger keeps slog.Default installed after a failed provide.

func SupplyOptions added in v0.4.0

func SupplyOptions(opts ...Option) fx.Option

SupplyOptions passes Options that need nothing from the Fx graph to Module. A plain fx.Supply does not reach it: Module reads the value group AsOptions and SupplyOptions contribute to. The Options of one call are applied in the order given; see AsOptions about the order between calls.

Types

type CallerConfig

type CallerConfig struct {
	Enabled *bool `env:"ENABLED" envDescription:"Include caller annotations; when unset, enabled at debug level and disabled otherwise."`
}

CallerConfig controls caller annotations. Unless Enabled is explicitly set, caller annotations are enabled at debug level and disabled otherwise.

type Config

type Config struct {
	// Level is debug, info, warn (or warning) or error; empty means info.
	Level  string       `env:"LEVEL" envDescription:"Log level: debug, info, warn, warning or error; unset or empty uses info."`
	Caller CallerConfig `envPrefix:"CALLER_"`
}

Config is the serializable logger configuration.

The tags are deliberately passive: zslogfx does not read the environment. Applications may fill Config with github.com/uchaloop/confmaker, another configuration library, or plain Go code. Register with the explicit instance name "log" to read LOG_*.

func (Config) Validate

func (c Config) Validate() error

Validate reports a level Make would reject. It is called automatically by confmaker, so a bad value fails the deployment rather than the first record. Make validates the level it ends up with, which an Option may have replaced.

type Logger

type Logger struct {
	Slog *slog.Logger
	// contains filtered or unexported fields
}

Logger wraps the *slog.Logger a service uses and the sync of the destination behind it. zap is only the backend.

func Make

func Make(cfg Config, opts ...Option) (*Logger, error)

Make builds a logger without installing it as slog.Default. It validates the settings Config and the Options add up to, so an Option really does override Config.

func (*Logger) Sync added in v0.4.0

func (l *Logger) Sync() error

Sync flushes what the destination itself buffers. The logger adds no buffer of its own, so no record waits for it. Sync may be called as often as the application likes, and the logger stays usable afterwards.

type Option

type Option func(*settings)

Option customizes logger construction after Config has been applied.

func WithCaller

func WithCaller(enabled bool) Option

WithCaller overrides Config.Caller.Enabled.

func WithFields

func WithFields(attrs ...slog.Attr) Option

WithFields adds slog attributes to every record. Options accumulate, so several calls add up.

func WithLevel

func WithLevel(level string) Option

WithLevel overrides Config.Level. An empty level, like an empty Config.Level, means info.

func WithStacktraceLevel

func WithStacktraceLevel(level slog.Level) Option

WithStacktraceLevel writes error.stack_trace on records at or above level. Without it no stack trace is written, and none is collected.

func WithWriteSyncer

func WithWriteSyncer(ws zapcore.WriteSyncer) Option

WithWriteSyncer replaces stdout as the destination. A nil syncer is ignored (stdout stays the default). The logger serializes writes to it, so ws does not have to be safe for concurrent use.

Jump to

Keyboard shortcuts

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