devlog

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2025 License: MIT Imports: 12 Imported by: 7

README

devlog

Go library that provides utilities for structured logging, building on the standard log/slog package. It provides two independent packages:

  • devlog implements a slog.Handler with a human-readable output format, designed for local development and CLI tools
  • devlog/log is a thin wrapper over the logging API of log/slog, providing:
    • Utility functions for log message formatting (log.Infof, log.Errorf, etc.)
    • Error-aware logging functions, which structure errors to be formatted consistently as log attributes
    • log.AddContextAttrs, a function for adding log attributes to a context.Context, applying the attributes to all logs made in that context

Run go get hermannm.dev/devlog to add it to your project!

Docs: pkg.go.dev/hermannm.dev/devlog

Contents:

Usage

Using the devlog output handler

devlog.Handler implements slog.Handler, so it can handle output for slog's logging functions. It can be configured as follows:

import (
	"log/slog"

	"hermannm.dev/devlog"
)

func main() {
	logHandler := devlog.NewHandler(os.Stdout, nil)
	slog.SetDefault(slog.New(logHandler))
}

Logging with slog will now use this handler. So the following log:

slog.Info("Server started", "port", 8000, "environment", "DEV")

...will give the following output (using a gruvbox terminal color scheme):

Screenshot of log message in a terminal

Structs, slices and other non-primitive types are encoded as pretty-formatted JSON, so this example:

type Event struct {
	ID   int    `json:"id"`
	Type string `json:"type"`
}
event := Event{ID: 1234, Type: "ORDER_UPDATED"}

slog.Error("Failed to process event", "event", event)

...gives this output:

Screenshot of log message in a terminal

devlog's output is meant to be easily read by a developer working locally. However, you may want a more structured format for production systems, to make log analysis easier. You can get both by conditionally choosing the log handler for your application, like this:

var logHandler slog.Handler
switch os.Getenv("ENVIRONMENT") {
case "LOCAL", "TEST":
	// Pretty-formatted logs for local development and tests
	logHandler = devlog.NewHandler(os.Stdout, nil)
default:
	// Structured JSON logs for deployed environments
	logHandler = slog.NewJSONHandler(os.Stdout, nil)
}
slog.SetDefault(slog.New(logHandler))
Using the devlog/log logging API

Unlike log/slog, devlog/log provides logging functions that take an error. When an error is passed to such a logging function, it is attached to the log as a cause attribute, so errors are structured consistently between logs.

import (
	"context"
	"errors"

	"hermannm.dev/devlog/log"
)

func example(ctx context.Context) {
	err := errors.New("database insert failed")
	log.Error(ctx, err, "Failed to store event")
}

This gives the following output (using the devlog output handler):

Screenshot of log message in a terminal

The package also provides log.AddContextAttrs, a function for adding log attributes to a context.Context. These attributes are added to all logs where the context is passed, so this example:

func processEvent(ctx context.Context, event Event) {
	ctx = log.AddContextAttrs(ctx, "eventId", event.ID)

	log.Debug(ctx, "Started processing event")
	// ...
	log.Debug(ctx, "Finished processing event")
}

...gives this output:

Screenshot of log messages in a terminal

This can help you trace connected logs in your system (especially when using a more structured JSON output in production, allowing you to filter on all logs with a specific eventId).

In order to encourage propagating context attributes, all log functions in this package take a context.Context. If you're in a function without a context parameter, you may pass a nil context. But ideally, you should pass a context wherever you do logging, in order to propagate context attributes.

Developer's guide

When publishing a new release:

  • Run tests:
    go test ./...
    
  • Add an entry to CHANGELOG.md (with the current date)
    • Remember to update the link section, and bump the version for the [Unreleased] link
  • Create commit and tag for the release (update TAG variable in below command):
    TAG=vX.Y.Z && git commit -m "Release ${TAG}" && git tag -a "${TAG}" -m "Release ${TAG}" && git log --oneline -2
    
  • Push the commit and tag:
    git push && git push --tags
    
    • Our release workflow will then create a GitHub release with the pushed tag's changelog entry

Credits

Documentation

Overview

Package devlog implements a structured log (slog) handler, with a human-readable output format designed for development builds.

A devlog.Handler can be configured as follows:

logHandler := devlog.NewHandler(os.Stdout, nil)
slog.SetDefault(slog.New(logHandler))

Following calls to log/slog's logging functions will use this handler, giving output on the following format:

slog.Info("Server started", "port", 8000, "environment", "DEV")
// [10:31:09] INFO: Server started
//   port: 8000
//   environment: DEV

Check the README to see the output format with colors.

To complement devlog's output handling, the hermannm.dev/devlog/log subpackage provides input handling. It is a thin wrapper over the slog package, with utility functions for log message and error formatting.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsColorTerminal added in v0.4.0

func IsColorTerminal(output io.Writer) bool

IsColorTerminal checks if the given writer is a terminal with ANSI color support. It respects NO_COLOR, FORCE_COLOR and TERM=dumb environment variables.

Types

type Handler

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

Handler is a slog.Handler that outputs log records in a human-readable format, designed for development builds. See the package-level documentation for more on the output format.

func NewHandler

func NewHandler(output io.Writer, options *Options) *Handler

NewHandler creates a log Handler that writes to output, using the given options. If options is nil, the default options are used.

func (*Handler) Enabled

func (handler *Handler) Enabled(_ context.Context, level slog.Level) bool

Enabled reports whether the handler is configured to log records at the given level.

func (*Handler) Handle

func (handler *Handler) Handle(_ context.Context, record slog.Record) error

Handle writes the given log record to the handler's output. See the devlog package docs for more on the output format.

func (*Handler) WithAttrs

func (handler *Handler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs returns a new Handler which adds the given attributes to every log record.

func (*Handler) WithGroup

func (handler *Handler) WithGroup(name string) slog.Handler

WithGroup returns a new Handler where all future log record attributes are nested under the given group name.

type Options

type Options struct {
	// Level is the minimum log record level that will be logged.
	// If nil, defaults to [slog.LevelInfo].
	Level slog.Leveler

	// AddSource adds a 'source' attr to every log record, with the file name and line number
	// where the log record was produced.
	// Defaults to false.
	AddSource bool

	// DisableColors removes colors from log output.
	//
	// Colors are enabled by default when the [io.Writer] given to [NewHandler] is a terminal with
	// color support (see [IsColorTerminal]).
	DisableColors bool

	// ForceColors skips checking [IsColorTerminal] for color support, and includes colors in log
	// output regardless. It overrides [Options.DisableColors].
	ForceColors bool

	// TimeFormat controls how time is formatted for each log entry. It defaults to
	// [TimeFormatShort], showing just the time and not the date, but can be set to [TimeFormatFull]
	// to include the date as well.
	TimeFormat TimeFormat
}

Options configure a log Handler.

type TimeFormat added in v0.5.0

type TimeFormat int8

TimeFormat is the type for valid constants for [Options.TimeFormat].

const (
	// TimeFormatShort includes just the time, not the date, formatted as: [10:57:30]
	//
	// This is the default time format.
	TimeFormatShort TimeFormat = iota

	// TimeFormatFull includes both date and time, formatted as: [2024-09-29 10:57:30]
	TimeFormatFull
)

Directories

Path Synopsis
Package log is a thin wrapper over the log/slog package.
Package log is a thin wrapper over the log/slog package.

Jump to

Keyboard shortcuts

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