log

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jun 15, 2023 License: Apache-2.0 Imports: 16 Imported by: 4

README

Contextual Logging

Intro

This library is a contextual logging library that makes use of context as part of the logging process. It is designed to make development easier by using the context variable to log instead of using a single global logger or passing a logger to every function.

Getting Started

package main
import (
    "context"

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

func main() {
    ctx := context.Background()
    logger := log.NewStandardLogger()

    // Setup with context fields.
    ctx = log.WithLogger(logger).With("key1", "val1").With("key2", "val2").Onto(ctx)

    // This is how you log.
    log.Debug(ctx, "Hello There!")
    // This is how you log with temporary extra fields.
    log.With("temporary", "fields").Info(ctx, "What's poppin?")
}

Why use this library

Do a lot of things in one operation

The library focuses on doing multiple operations, whether it is adding fields or configuring the logger, in one chained operation. This makes setup and logging very simple, especially when they involve Fields.

Shallow Context Tree

Fields are stored in the context tree when you use the Onto method. Doing many things in one operation allows you to produce a shallow context tree as you do not need to add Fields one-by-one. By finalizing the Fields operation using Onto, it ensures that it will only add all the provided Fields once. This is extremely beneficial when several fields must be logged, which is common in large and complex codebases.

Greater control over Fields and Logger

There are also many operations you can do on Fields as the library allows you to store fields in a variable for finer control. The With methods allow many different types of Fields to be entered and APIs like Chain and Suppress make Fields a lot more customizable.

Immutability

The library ensures that Fields are immutable and the real Logger is never exposed. Any access to the logger will return a copy. This is very beneficial in programs with concurrent processes.

Customisable

The library provides a lot of ways to customize your logger to meet your needs. You can create your own configuration or even an entirely different logger. The provided interfaces are small which makes it really easy to create your own configurations to the library.

Compared to other solutions

A very popular solution for logging in open source is the logrus library. While it is a great logging library, it does not provide a built-in Fields solution and a very high level of Fields manipulation. It also does not implement context properly as it requires you to create a custom format even after using their WithContext API. Finally, logrus has a large API, and while it provides a great amount of features, users can find it intimidating and confusing to use.

Compared to logrus, the library provides a built-in solution in implementing context, the provided default formats ensure that context values that you need are logged. The library also provides a simple set of APIs that are easy to use. Everything you need that involves a logger, you can easily find it.

Main Features

Fields

Fields are key value data that are logged along with the log message. This library makes manipulating Fields easier and more flexible. With this library, everything is treated a Field, that includes Fields themselves, logger configuration, and even the logger itself. This makes it possible for you to create everything in one chained operation.

    f := log.With("key1", "value1")

There is also the context fields where it will take values that correspond to the given context key. The context key can be any object but you have to provide the alias for the key. If the key does not have any value in the context, it will not be logged.

    f = log.WithCtxRef("alias", ctxKey{})

You can also add multiple Fields by chaining the operation.

    f = f.
       With("another", "key").
          With("more", 1).
       With("more key", 'q')

One thing to remember, since fields are key value data, in the event of overlapping keys, values will be replaced based on the order of operation. In a chain operation, the later operations have higher precedence and will replace the keys. At this example, the value that corresponds to another is now fields instead of key.

    f = f.With("another", "fields")

As mentioned before, everything is treated as fields and that includes the logger and its configuration. Only one logger can be in fields and one of each type of configurations (e.g. only one rule for format etc). Because they are fields they will also follow the precedence rule, which means adding another logger or a configuration type will replace the older values.

    f = f.
       WithLogger(log.NewStandardLogger()).
          WithConfigs(log.NewJSONFormat())

The fields then can be used to log directly (example in later section) or they can be saved in the context for later use by using the Onto API.

    newCtx := f.Onto(ctx)

A couple more useful APIs to know.

Suppress will ensure that the provided keys will not be logged. In this example, the key another, more key, and alias will not be logged. For context reference fields, you have to refer to them by their alias.

    f = f.Suppress("another", "more key", "alias")

Chain provides a way of merging multiple fields. Just like before, precedence gets higher from left to right.

    f1 := log.With("key1", "value1")
    f2 := log.With("key2", "value2")
    f3 := log.With("key3", "value3")
    f = f.Chain(f1, f2, f3)

A very important thing to note is that Fields are immutable which makes them thread-safe but it also means that you need to receive the returned value of fields operation as they do not mutate themselves.

Logging

Logging can be accessed through the Debug, Info, and Error API. Each of them also have their format function counterpart which are Debugf, Infof, and Errorf. Debug and Debugf is only logged when the logger is in the verbose mode while the others will always be logged. Each of the log functions require a context to be passed in. If the context contains fields, that fields will be logged along with the message given. If the context does not contain a logger a standard logger will be provided as the default.

    log.Debug(ctx, "this is debug")
    log.Debugf(ctx, "%s with format", "this is debug")
    log.Info(ctx, "this is info")
    log.Infof(ctx, "%s with format", "this is info")

For Error and Errorf, the error variable is required. The error message will be logged as a field with the key of error_message.

    log.Error(ctx, errors.New("error"), "this is error")
    log.Errorf(ctx, errors.New("error"), "%s with format", "this is error")

If you would like to log certain fields without adding additional fields to the context, you can do so by using the same API on the additional fields. Additional fields are merged with the fields in context if the context contains fields and it also has higher precedence but they do not mutate the fields in context.

    log.With("additional", "fields").With("more", "fields").Debug(ctx, "debug")
    log.With("additional", "fields").With("more", "fields").Debugf(ctx, "formatted %s", "debug")

     // This log will only log fields inside the context.
    log.Debug(ctx, "no additional fields")

Should you require the logger object itself, you can do so by using the From API which will extract the logger in the context. If context does not have any logger, it will returns a new standard logger. The returned logger is copied for immutability. The logger returned by From have all the fields and configuration applied to it. The fields are also resolved, meaning any context reference will use any value in the context at the time of call.

    logger := log.From(ctx)

    // This one will return a logger with the additional fields merged with context fields
    logger := log.With("extra", "fields").From(ctx)
Configuring logger

Logger configurations are treated as fields. This can be done through the WithConfigs API. You can add multiple configurations in a single WithConfigs operation. The configurations can also be saved in a context along with other fields. Even if you replace the logger, the configurations stay and will always be applied to the logger. Only one type of each configuration type can exist in a fields. If another config of the same type is added, it will replace the old one.

    // This adds the JSON formatter to the logger.
    f = log.WithConfigs(log.NewJSONFormat())

    // This will replace JSON formatter.
    f = log.WithConfigs(log.NewStandardFormat())

    // You can add multiple configurations
    f = log.WithConfigs(log.NewJSONFormat(), log.NewStandardFormat())
Logging Format

Currently there is only one logger which is the StandardLogger which uses logrus. The provided formatter implements logrus formatter system. There are two formatters, the JSON formatter and the Standard formatter (which is the default formatter when no configuration is added).

JSON format

JSON formatter will log in the following format:

{
 "fields": {
  "key1": "value1", // value can be any data types
  "key2": "value2",
 },
 "level": "log level", // string, either INFO or DEBUG
 "message": "log message", // string,
 "timestamp": "log time", // timestamp in RFC3339Nano format
}

Fields will be logged as an object of the attribute fields. One thing to remember is that, for context reference, the key will use the provided alias.

Standard format

The standard formatter will log in the following format without the parentheses:

(time in RFC3339Nano Format) (Fields) (Level) (Message)

For example:

2020-02-05T09:05:11.041651+11:00 this=one have=fields INFO log with fields

In the current implementation, the fields are logged in a random order.

Verbosity

Setting the verbose mode of the logger will log debug entries:

    ctx := context.Background()

    // By default, the logger will not log debug entries
    log.Info(ctx, "not logged")

    // Make the logger log debug level entries
    ctx = log.WithConfigs(log.SetVerboseMode(true)).Onto(ctx)
    
    // With verbose mode enabled, the logger will log debug entries
    log.Info(ctx, "logged")
Log Caller

Setting the logger to log the caller will include the a reference to the source from which the log was called:

    ctx := context.Background()

    // By default, the logger will not log the caller
    // 2020-02-05T09:05:11.041651+11:00 INFO one
    log.Info(ctx, "one")

    // Make the logger log the caller
    ctx = log.WithConfigs(log.SetLogCaller(true)).Onto(ctx)
    
    // With caller log enabled, the logger will log the caller
    // 2020-02-05T09:05:11.041651+11:00 INFO two [/path/to/example.go:42]
    log.Info(ctx, "two")
Hook

Hooks can be added to the logger that are notified when an entry is logged:

    type myHook struct { }
    func (h *myHook) OnLogged(entry *LogEntry) error { ... }

    ctx = log.WithConfigs(log.AddHook(myHook{})).Onto(context.Background())
    log.Info(ctx, "message") // log entry sent to hook
Custom configuration

It is possible to create your own configuration. You will have to create an object that implements the provided interface.

type Config interface {
 TypeKey() interface{}
 Apply(logger Logger) error
}

TypeKey() returns the type of the configuration and Apply() will apply the configuration to the logger. For formatters, use the FormatterType type key provided by the library to ensure that it is recognized as a formatter.

Documentation

Index

Constants

View Source
const (
	FormatterType typeKey = iota
)

Variables

This section is empty.

Functions

func Debug

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

Debug logs from context at the debug level.

func Debugf

func Debugf(ctx context.Context, format string, args ...interface{})

Debugf logs from context at the debug level.

func Error

func Error(ctx context.Context, err error, args ...interface{})

Error logs from context at the info level with the error_message fields.

func Errorf

func Errorf(ctx context.Context, err error, format string, args ...interface{})

Errorf logs from context at the info level with the error_message fields.

func Info

func Info(ctx context.Context, args ...interface{})

Info logs from context at the debug level.

func Infof

func Infof(ctx context.Context, format string, args ...interface{})

Infof logs from context at the debug level.

Types

type AddableHooks

type AddableHooks interface {
	// AddHooks adds the given hooks to the logger.
	AddHooks(hooks ...Hook) error
}

type CodeReference

type CodeReference struct {

	// Path of the file (within the local file system) where the source code is found
	File string

	// Line number (1-indexed) within the source code file
	Line int
}

CodeReference describes a reference to a point within a source code file.

type Config

type Config interface {
	TypeKey() interface{}
	Apply(logger Logger) error
}

func AddHooks

func AddHooks(hooks ...Hook) Config

AddHooks adds the given hooks to the logger.

func NewJSONFormat

func NewJSONFormat() Config

func NewStandardFormat

func NewStandardFormat() Config

func SetLogCaller

func SetLogCaller(on bool) Config

SetLogCaller sets whether or not a reference to the calling function is logged.

func SetOutput

func SetOutput(w io.Writer) Config

func SetVerboseMode

func SetVerboseMode(on bool) Config

type Fields

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

Fields is a struct that contains all the fields data to log.

func FieldsFrom added in v0.0.29

func FieldsFrom(ctx context.Context) Fields

FieldsFrom retrieves the fields from the context.

func Suppress

func Suppress(keys ...string) Fields

Suppress will ensure that suppressed keys are not logged.

func With

func With(key string, val interface{}) Fields

With creates a field with a single key value pair.

func WithConfigs

func WithConfigs(configs ...Config) Fields

WithConfigs adds extra configuration for the logger.

func WithContextKey

func WithContextKey(key string, ctxKey interface{}) Fields

WithContextKey creates a field with a key that refers to the provided context key, fields will use key as the fields property and take the value that corresponds to ctxKey.

func WithLogger

func WithLogger(logger Logger) Fields

WithLogger adds logger which will be used for the log operation.

func (Fields) Chain

func (f Fields) Chain(fieldses ...Fields) Fields

Chain merges all the fields and returns the merged fields, the precedence of fields in case of overlapping gets higher from left to right.

func (Fields) Debug

func (f Fields) Debug(ctx context.Context, args ...interface{})

Debug logs from context at the debug level.

func (Fields) Debugf

func (f Fields) Debugf(ctx context.Context, format string, args ...interface{})

Debugf logs from context at the debug level.

func (Fields) Error

func (f Fields) Error(ctx context.Context, errMsg error, args ...interface{})

Error logs from context at the info level with the error_message fields.

func (Fields) Errorf

func (f Fields) Errorf(ctx context.Context, errMsg error, format string, args ...interface{})

Errorf logs from context at the info level with the error_message fields.

func (Fields) From

func (f Fields) From(ctx context.Context) Logger

From returns a logger with the new fields which is the fields from the context merged with the current fields were current fields replaces value from the context fields.

func (Fields) Info

func (f Fields) Info(ctx context.Context, args ...interface{})

Info logs from context at the debug level.

func (Fields) Infof

func (f Fields) Infof(ctx context.Context, format string, args ...interface{})

Infof logs from context at the debug level.

func (Fields) MergedString

func (f Fields) MergedString(ctx context.Context) string

MergedString returns a string that represents the current fields merged by fields in context

func (Fields) Onto

func (f Fields) Onto(ctx context.Context) context.Context

Onto finishes fields operation, merge them all with the precedence of fields in case overlapping gets higher from left to right, and puts the merged fields in the context.

func (Fields) String

func (f Fields) String(ctx context.Context) string

String returns a string that represent the current fields

func (Fields) Suppress

func (f Fields) Suppress(keys ...string) Fields

Suppress ensures that the keys will not be logged.

func (Fields) With

func (f Fields) With(key string, val interface{}) Fields

With adds to the fields a single key value pair.

func (Fields) WithConfigs

func (f Fields) WithConfigs(configs ...Config) Fields

WithConfigs adds extra configuration for the logger.

func (Fields) WithContextKey

func (f Fields) WithContextKey(key string, ctxKey interface{}) Fields

WithContextKey adds key and the context key to the fields.

func (Fields) WithLogger

func (f Fields) WithLogger(logger Logger) Fields

WithLogger adds logger which will be used for the log operation.

type Formattable

type Formattable interface {
	// SetFormatter sets the formatter for the logger.
	// The formatter provided must also implement the Formatter interface.
	SetFormatter(formatter Config) error
}

type Formatter

type Formatter interface {
	// Format translates a log entry into a string representation
	Format(*LogEntry) (string, error)
}

type Hook

type Hook interface {
	OnLogged(*LogEntry) error
}

Hook describes a callback to receive notice when an entry is logged

func NewForwardingHook added in v0.0.7

func NewForwardingHook(logger Logger) Hook

NewForwardingHook returns a Hook that forwards all entries to the given Logger.

type LogEntry

type LogEntry struct {

	// Time at which the log entry was created
	Time time.Time

	// Message passed to Debug, Info or Error
	Message string

	// Data set by the user.
	Data frozen.Map[any, any]

	// Source code reference of the calling function.
	// Initialised if SetLogCaller is true.
	Caller CodeReference

	// True if the log is verbose (Debug), false otherwise (Info or Error)
	Verbose bool
}

LogEntry describes an entry to log.

type Logger

type Logger interface {
	// Debug logs the message at the Debug level.
	Debug(args ...interface{})
	// Debugf logs the message at the Debug level.
	Debugf(format string, args ...interface{})
	// Error logs the message at the Error level
	Error(errMsg error, args ...interface{})
	// Errorf logs the message at the Error level
	Errorf(errMsg error, format string, args ...interface{})
	// Info logs the message at the Info level
	Info(args ...interface{})
	// Infof logs the message at the Info level.
	Infof(format string, args ...interface{})
}

Logger is the underlying logger that is to be added to a context.

func From

func From(ctx context.Context) Logger

From returns a copied logger from the context that you can use to access logger API.

func NewNullLogger

func NewNullLogger() Logger

Create a null logger that doesn't log

func NewStandardLogger

func NewStandardLogger() Logger

NewStandardLogger returns a logger with a standard formatter

type SettableLogCaller

type SettableLogCaller interface {
	// SetLogCaller sets whether or not a reference to the calling function is logged.
	SetLogCaller(on bool) error
}

type SettableOutput

type SettableOutput interface {
	// SetOutput sets where the logger outputs to.
	SetOutput(w io.Writer) error
}

type SettableVerbosity

type SettableVerbosity interface {
	// SetVerbose sets the verbosity of the logger.
	SetVerbose(on bool) error
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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