logging

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Mar 4, 2026 License: Apache-2.0 Imports: 11 Imported by: 2

README

Logging

Integrate context with your logs.

Get

go get -u github.com/anz-bank/pkg/logging

Overview

The problem

I want to add contextual data to my logs, but all other packages out there require many function calls just to perform one log.

The solution

Use a logger that understands context.

What this package provides
  1. Easy api to add log context to a context and include it in your logs down the line.
  2. A standardized approach to how this is achieved, so it can be used across packages and projects.
  3. Integration with many third party libraries that take loggers

Documentation

Overview

Package logging handles your application logs. Logging is built upon the very popular 'zerolog' package and extends it by making it easier to manage logging context.

This module provides a thin wrapper on top of zerolog that allows you to set up contextual logging, and code links We expose some of the zerolog API for adding fields to logs, so it can help to understand zerolog.

https://github.com/rs/zerolog

Logging API

Logging is built around the zerolog.Event type. To log something, invoke a log event through one of the base log functions or their equivalents on a logger object.

logging.Info
logging.Debug
logging.Error

Any fields defined in context will already be added to this event, but you can add more. See the zerolog.Event docs or examples. To close off the event and log a message, use the Msg method. A complete example might look like.

logging.Error(ctx, err).             // Invoke a log event
  Str("failure_type", "validation"). // Add a field
  Msg("Request failed")              // execute log with msg

JSON logs

By default the logging package produces logs in JSON format. Every piece of log information has an associated key, and you can define nested objects and arrays

{
  "time": "timestamp",
  "level": "info",
  "message": "Hello World",
  "service": {
    "name": "service",
    "version": "v1.0.0"
  },
  "tags": ["t1", "t2"]
}

Contextual logging

Contextual logging the the key feature this library provides over zerolog. It refers to the ability to create loggers with values takes dynamically from context. The key to doing this is using this libraries Context object.

type Context struct {
	logger *Logger
	funcs  []ContextFunc
}

This object acts as a go-between for loggers and context. Create a logging context by adding ContextFuncs to a logger. You can then either add it to a real context or create a new logger with values loaded from context.

logCtx := logger.With(ContextFunc...)
ctx = logCtx.ToContext(ctx)      // adds logging context to context
logger = logCtx.FromContext(ctx) // loads values from context to create a logger with context values set

ToContext allows you to use the package log functions later without worrying about passing values in or calling the correct function to retrieve values before your log.

ctx = logCtx.ToContext(ctx)
logging.Info(ctx).Msg("Hello World")

Duplicate fields

Zerolog provides zero protection from duplicate fields. If you are not careful, you may accidentally run into this problem. This package provides one level of protection from field duplication, identifiers for context funcs. Each context func has an identifier that the logger uses to discover duplicate.

logger := logger.With(Str("foo", "bar"), Str("foo", "baz"))
// {"foo","bar"} no duplicate

This does NOT completely protect from duplication. Fields added after context information has been collected CAN still result in duplications

logger := logger.With(Str("foo", "bar"))
logger.Info(ctx).Str("foo", "baz").Msg("Hello World")
// {"foo":"bar","foo":"baz","msg":"Hello World"}

Custom context funcs CAN also still accidentally duplicate fields if they set the same fields but have different function identifiers. There are two rules to help avoid this

1. All common packages that define context funcs MUST document the fields they set.

// MyContextFunc sets fields to log xyz
//
// Field abc - short description
//
// Field xyz - short description
// ...

2. Make sure you context funcs declare the same keys it intends to set, its easy for typos to creep in.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoLoggerInContext is used when a log call occurs on a context with no logger
	ErrNoLoggerInContext = errors.New("log: context has no logger")
)

Functions

func Array

func Array() *zerolog.Array

Array defines the value for an array field

returns a LogArrayMarshaller. Use methods on this to add array elements to an array entry

func Debug

func Debug(ctx context.Context, args ...interface{}) *zerolog.Event

Debug extracts the logger from context and logs a message at the Debug level

func Dict

func Dict() *zerolog.Event

Dict defines the value for a dict field

returns a zerolog.logCtx. Use the logCtx methods to add sub fields to a dict entry

func Error

func Error(ctx context.Context, err error) *zerolog.Event

Error extracts the logger from context and logs a message at the Error level

func Info

func Info(ctx context.Context) *zerolog.Event

Info extracts the logger from context and logs a message at the Info level

Example
package main

import (
	"context"
	"io"
	"os"

	"github.com/anz-bank/pkg/logging"
)

func testContext(out io.Writer) context.Context {
	logger := logging.New(out).WithLevel(logging.DebugLevel)
	return logger.With().ToContext(context.Background())
}

func main() {
	// Standard log calls will look like this.
	// MAKE SURE your context has a logger in it, otherwise it will panic
	logging.Info(testContext(os.Stdout)).Msg("Hello World")
}
Output:
{"level":"info","message":"Hello World"}
Example (EmbeddedObject)
package main

import (
	"context"
	"io"
	"os"

	"github.com/anz-bank/pkg/logging"
)

func testContext(out io.Writer) context.Context {
	logger := logging.New(out).WithLevel(logging.DebugLevel)
	return logger.With().ToContext(context.Background())
}

func main() {
	// You can also embed an object
	logging.Info(testContext(os.Stdout)).
		Dict("subfields", logging.Dict().
			Str("field1", "foo").
			Int("field2", 123),
		).
		Msg("Hello World")
}
Output:
{"level":"info","subfields":{"field1":"foo","field2":123},"message":"Hello World"}
Example (ExtraFields)
package main

import (
	"context"
	"io"
	"os"

	"github.com/anz-bank/pkg/logging"
)

func testContext(out io.Writer) context.Context {
	logger := logging.New(out).WithLevel(logging.DebugLevel)
	return logger.With().ToContext(context.Background())
}

func main() {
	// You can add log specific fields
	logging.Info(testContext(os.Stdout)).Int("count", 1).Msg("Hello World")
}
Output:
{"level":"info","count":1,"message":"Hello World"}

func SetGlobalLogContext

func SetGlobalLogContext(logCtx *Context)

SetGlobalLogContext sets the global logger to the given logger

Allows adding static application values to a logger

Types

type Context

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

Context creates loggers from context

This type acts as a go-between between loggers and context. Context is able to create a logger with values extracted from context according to the stored ContextFuncs.

Context can also be added to context itself to support the main log functions (eg: logging.Info(ctx))

func ContextFromContext

func ContextFromContext(ctx context.Context) *Context

ContextFromContext returns a Context from context

If no log context is defined, this will return the global log context

func (Context) FromContext

func (c Context) FromContext(ctx context.Context) *Logger

FromContext creates a logger from the given context and Context

func (Context) ToContext

func (c Context) ToContext(ctx context.Context) context.Context

func (Context) With

func (c Context) With(funcs ...ContextFunc) *Context

With adds context funcs to the log context

func (Context) WithArray

func (c Context) WithArray(key string, val zerolog.LogArrayMarshaler) *Context

WithArray creates a static array field on the logger

func (Context) WithBool

func (c Context) WithBool(key string, val bool) *Context

WithBool creates a static boolean field on the logger

func (Context) WithDict

func (c Context) WithDict(key string, val *zerolog.Event) *Context

WithDict creates a static dictionary field (json object) on the log context

func (Context) WithDur

func (c Context) WithDur(key string, val time.Duration) *Context

WithDur creates a static duration field on the logger

func (Context) WithInt

func (c Context) WithInt(key string, val int) *Context

WithInt creates a static int field on the log context

func (Context) WithStr

func (c Context) WithStr(key string, val string) *Context

WithStr creates a static string field on the log context

func (Context) WithTime

func (c Context) WithTime(key string, val time.Time) *Context

WithTime creates a static time field on the logger

type ContextFunc

type ContextFunc struct {
	Keys     []string
	Function func(ctx context.Context, event zerolog.Context) zerolog.Context
}

ContextFunc defines the function signature used by the logger to extract fields from context

Includes an ID that loggers use to prevent duplication

type Level

type Level uint32

Level types the level construct

Mirrors zerolog.Level

const (
	DebugLevel Level = Level(zerolog.DebugLevel)
	InfoLevel  Level = Level(zerolog.InfoLevel)
	ErrorLevel Level = Level(zerolog.ErrorLevel)
)

zerolog level constants included here. Use these to avoid importing logrus directly Not all levels are included, only those used by this library

func MustParseLevel

func MustParseLevel(s string) Level

MustParseLevel panics if input is not a valid level string

func ParseLevel

func ParseLevel(s string) (Level, error)

ParseLevel parses a level string and returns the equivalent level value

Valid levels are info, debug, error. NOT CASE SENSITIVE

func (Level) String

func (l Level) String() string

type Logger

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

Logger is responsible for executing log calls

This logger is a wrapper on zerolog.Logger that adds contextual logging and code linking. Invoke a log with one of Info, Debug or Error, and use the zerolog api to add fields and execute the log.

eg: logger.Info(ctx).Str("key", "value").Msg("Hello World")

Add contextual logging with the With method. This takes an array of ContextFunc function instances to call on every log call. This should be done to set up the logger before it is used, usually in application init.

Loggers are immutable. Mutations return a new logger with the mutation applied

Example (DuplicateKeys)
package main

import (
	"os"

	"github.com/anz-bank/pkg/logging"
)

func main() {
	// logger fields are protected from duplication
	logger := logging.New(os.Stdout).
		WithStr("foo", "bar").
		WithStr("foo", "baz")

	// log contains a single 'foo' field
	logger.Info().Msg("Hello World")

	// Unfortunately there is no duplication protection after context has been applied
	logger.Info().Str("foo", "baz").Msg("Hello World")
}
Output:
{"level":"info","foo":"bar","message":"Hello World"}
{"level":"info","foo":"bar","foo":"baz","message":"Hello World"}
Example (StaticFields)
package main

import (
	"os"

	"github.com/anz-bank/pkg/logging"
)

func main() {
	// logging provides a few convenience functions for static fields
	logger := logging.New(os.Stdout).
		WithStr("string", "foo").
		WithInt("some_int", 1).
		WithDict("metadata", logging.Dict().
			Str("service", "service_name").
			Str("version", "v1.0.0"),
		)

	logger.Info().Msg("Hello World")
}
Output:
{"level":"info","string":"foo","some_int":1,"metadata":{"service":"service_name","version":"v1.0.0"},"message":"Hello World"}

func FromContext

func FromContext(ctx context.Context) *Logger

FromContext returns a logger with values taken from context

If no log context is defined, this will return a logger created from the global log context

func New

func New(out io.Writer) *Logger

New returns a new logger

func (*Logger) Debug

func (l *Logger) Debug() *zerolog.Event

Debug logs a message at the debug level

func (Logger) Discard

func (l Logger) Discard() *Logger

Discard causes any log call on the logger to be ignored

This is useful for creating a logger that shhould not log depending on some criteria

if cond { logger = logger.Discard() }

func (*Logger) Error

func (l *Logger) Error(err error) *zerolog.Event

Error logs a message at the error level

func (*Logger) Info

func (l *Logger) Info() *zerolog.Event

Info logs a message at the info level

func (Logger) ToContext

func (l Logger) ToContext(ctx context.Context) context.Context

ToContext adds the logger to context

func (Logger) With

func (l Logger) With(funcs ...ContextFunc) *Context

With adds ContextFuncs to the logger

Use context funcs to add any field to the logger, static or contextual.

Example
package main

import (
	"context"
	"os"

	"github.com/anz-bank/pkg/logging"
	"github.com/rs/zerolog"
)

type keyStruct struct{}

var key = &keyStruct{}

func main() {
	// With adds a ContextFunc to the logger. This is invoked on every log call
	// The ID is a unique identifier for the function.
	// This function will log a 'context_value' if it exists in context
	ctx := logging.New(os.Stdout).With(
		logging.ContextFunc{
			Keys: []string{"context_value"},
			Function: func(ctx context.Context, logCtx zerolog.Context) zerolog.Context {
				ctxValue, ok := ctx.Value(key).(string)
				if ok {
					logCtx = logCtx.Str("context_value", ctxValue)
				}
				return logCtx
			},
		},
	).ToContext(context.Background())

	// this context does not have a value set yet so it won't be logged
	logging.Info(ctx).Msg("Hello World")

	// This context does have it. We don't need to manually extract the value
	ctx = context.WithValue(ctx, key, "I am contextual")
	logging.Info(ctx).Msg("Hello World")
}
Output:
{"level":"info","message":"Hello World"}
{"level":"info","context_value":"I am contextual","message":"Hello World"}

func (Logger) WithArray

func (l Logger) WithArray(key string, val zerolog.LogArrayMarshaler) *Logger

WithArray creates a static array field on the logger

func (Logger) WithBool

func (l Logger) WithBool(key string, val bool) *Logger

WithBool creates a static boolean field on the logger

func (l Logger) WithCodeLinks(links bool, linker codelinks.CodeLinker) *Logger

WithCodeLinks sets the code linker

set 'links' to false to turn links off

func (Logger) WithDict

func (l Logger) WithDict(key string, val *zerolog.Event) *Logger

WithDict creates a static dictionary field (json object) on the logger

func (Logger) WithDur

func (l Logger) WithDur(key string, val time.Duration) *Logger

WithDur creates a static duration field on the logger

func (Logger) WithInt

func (l Logger) WithInt(key string, val int) *Logger

WithInt creates a static int field on the logger

func (Logger) WithLevel

func (l Logger) WithLevel(level Level) *Logger

WithLevel creates a copy of the logger with a new log level

func (Logger) WithOutput

func (l Logger) WithOutput(out io.Writer) *Logger

WithOutput creates a copy of the logger with a new writer to write logs to

func (Logger) WithStr

func (l Logger) WithStr(key string, val string) *Logger

WithStr creates a static string field on the logger

func (Logger) WithTime

func (l Logger) WithTime(key string, val time.Time) *Logger

WithTime creates a static time field on the logger

func (Logger) WithTimeDiff

func (l Logger) WithTimeDiff(key string, start time.Time) *Logger

WithTimeDiff adds a time diff field to the logger

Example
package main

import (
	"os"
	"time"

	"github.com/anz-bank/pkg/logging"
)

func main() {
	logger := logging.New(os.Stdout).WithTimeDiff("time_since_request_start", time.Now())

	// This log time_since_request_start field will be fairly small
	logger.Info().Msg("Hello World")

	// This log time_since_request_start field will have progressed by approx 10ms
	time.Sleep(10 * time.Millisecond)
	logger.Info().Msg("Hello World")
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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