ctxd

package module
v1.2.1 Latest Latest
Warning

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

Go to latest
Published: Jan 29, 2023 License: MIT Imports: 9 Imported by: 51

README

Contextualized Structured Logging and Error Handling for Go

This library provides context-driven structured error and logger contracts.

Build Status Coverage Status GoDevDoc time tracker Code lines Comments

Usage

  • Create an adapter for your logger that implements ctxd.Logger or use zapctxd that is built around awesome go.uber.org/zap.
  • Add fields to context and pass it around.
  • Use context for last-mile logging or error emitting.

Example

Structured Logging
logger := ctxd.LoggerMock{}

// Once instrumented context can aid logger with structured information.
ctx := ctxd.AddFields(context.Background(), "foo", "bar")

logger.Info(ctx, "something happened")

// Also context contributes additional information to structured errors.
err := ctxd.NewError(ctx, "something failed", "baz", "quux")

ctxd.LogError(ctx, err, logger.Error)

fmt.Print(logger.String())
// Output:
// info: something happened {"foo":"bar"}
// error: something failed {"baz":"quux","foo":"bar"}

Logger can be instrumented with persistent fields that are affecting every context.

lm := ctxd.LoggerMock{}

var globalLogger ctxd.Logger = &lm

localLogger := ctxd.LoggerWithFields(globalLogger, "local", 123)

ctx1 := ctxd.AddFields(context.Background(),
    "ctx", 1,
    "foo", "bar",
)
ctx2 := ctxd.AddFields(context.Background(), "ctx", 2)

localLogger.Info(ctx1, "hello", "he", "lo")
localLogger.Warn(ctx2, "bye", "by", "ee")

fmt.Print(lm.String())

// Output:
// info: hello {"ctx":1,"foo":"bar","he":"lo","local":123}
// warn: bye {"by":"ee","ctx":2,"local":123}
Handling Errors
ctx := context.Background()

// Elaborate context with fields.
ctx = ctxd.AddFields(ctx,
    "field1", 1,
    "field2", "abc",
)

// Add more fields when creating error.
err := ctxd.NewError(ctx, "something failed",
    "field3", 3.0,
)

err2 := ctxd.WrapError(
    // You can use same or any other context when wrapping error.
    ctxd.AddFields(context.Background(), "field5", "V"),
    err, "wrapped",
    "field4", true)

// Setup your logger.
var (
    lm                 = ctxd.LoggerMock{}
    logger ctxd.Logger = &lm
)

// Inspect error fields.
var se ctxd.StructuredError
if errors.As(err, &se) {
    fmt.Printf("error fields: %v\n", se.Fields())
}

// Log errors.
ctxd.LogError(ctx, err, logger.Error)
ctxd.LogError(ctx, err2, logger.Warn)
fmt.Print(lm.String())

// Output:
// error fields: map[field1:1 field2:abc field3:3]
// error: something failed {"field1":1,"field2":"abc","field3":3}
// warn: wrapped: something failed {"field1":1,"field2":"abc","field3":3,"field4":true,"field5":"V"}

Documentation

Overview

Package ctxd defines contextualized structured logging and error handling.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddFields

func AddFields(ctx context.Context, keysAndValues ...interface{}) context.Context

AddFields returns context with added loosely-typed key-value pairs as fields.

If key-value pairs exist in parent context already, new pairs are appended.

Example
package main

import (
	"context"
	"fmt"

	"github.com/bool64/ctxd"
)

func main() {
	logger := ctxd.LoggerMock{}

	// Once instrumented context can aid logger with structured information.
	ctx := ctxd.AddFields(context.Background(), "foo", "bar")

	logger.Info(ctx, "something happened")

	// Also context contributes additional information to structured errors.
	err := ctxd.NewError(ctx, "something failed", "baz", "quux")

	ctxd.LogError(ctx, err, logger.Error)

	fmt.Print(logger.String())
}
Output:

info: something happened {"foo":"bar"}
error: something failed {"baz":"quux","foo":"bar"}

func ClearFields

func ClearFields(ctx context.Context) context.Context

ClearFields returns context without any fields.

func Fields

func Fields(ctx context.Context) []interface{}

Fields returns loosely-typed key-value pairs found in context or nil.

func IsDebug

func IsDebug(ctx context.Context) bool

IsDebug returns true if debug flag is enabled in context.

func LabeledError added in v1.1.0

func LabeledError(err error, labels ...error) error

LabeledError adds indicative errors to an error wrap.

Labels could be checked with errors.Is, errors.As. Error message remains the same with original error.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/bool64/ctxd"
)

func main() {
	err1 := errors.New("failed")
	label1 := ctxd.SentinelError("miserably")
	label2 := ctxd.SentinelError("hopelessly")

	err := ctxd.LabeledError(fmt.Errorf("oops: %w", err1), label1, label2)

	fmt.Println("err is err1:", errors.Is(err, err1))
	fmt.Println("err is label1:", errors.Is(err, label1))
	fmt.Println("err is label2:", errors.Is(err, label2))

	// Labels do not implicitly contribute to error message.
	fmt.Println("error message:", err.Error())

	// If there are two matches, only first is returned.
	var se ctxd.SentinelError

	fmt.Println("err as &se:", errors.As(err, &se))
	fmt.Println("err as value:", string(se))

}
Output:

err is err1: true
err is label1: true
err is label2: true
error message: oops: failed
err as &se: true
err as value: miserably

func LogError

func LogError(ctx context.Context, err error, l LogFunc)

LogError pushes error value to a contextualized logger method.

If err is nil, LogError produces no operation. LogError function matches Logger methods, e.g. Error.

func LogWriter

func LogWriter(ctx context.Context) io.Writer

LogWriter returns custom log writer found in context or nil.

func MultiError added in v1.2.0

func MultiError(primary error, secondary ...error) error

MultiError creates an error with multiple unwrappables.

Secondary errors could be checked with errors.Is, errors.As. Error message remains the same with primary error.

Multi errors can be used to augment error with multiple checkable perks, without a limitation of single wrapping inheritance.

func NewError

func NewError(ctx context.Context, message string, keysAndValues ...interface{}) error

NewError creates error with optional structured data.

LogError fields from context are also added to error structured data.

func SetFields added in v0.1.4

func SetFields(ctx context.Context, keysAndValues ...interface{}) context.Context

SetFields returns context with added loosely-typed key-value pairs as fields.

Values of same keys that are already existing in parent context are replaced.

func WithDebug

func WithDebug(ctx context.Context) context.Context

WithDebug returns context with debug flag enabled.

func WithLogWriter

func WithLogWriter(ctx context.Context, w io.Writer) context.Context

WithLogWriter returns context with custom log writer. Can be useful to write logs into response stream.

func WrapError

func WrapError(ctx context.Context, err error, message string, keysAndValues ...interface{}) error

WrapError returns an error annotated with message and structured data.

If err is nil, WrapError returns nil. LogError fields from context are also added to error structured data.

Example
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/bool64/ctxd"
)

func main() {
	ctx := context.Background()

	// Elaborate context with fields.
	ctx = ctxd.AddFields(ctx,
		"field1", 1,
		"field2", "abc",
	)

	// Add more fields when creating error.
	err := ctxd.NewError(ctx, "something failed",
		"field3", 3.0,
	)

	err2 := ctxd.WrapError(
		// You can use same or any other context when wrapping error.
		ctxd.AddFields(context.Background(), "field5", "V"),
		err, "wrapped",
		"field4", true)

	// Setup your logger.
	var (
		lm                 = ctxd.LoggerMock{}
		logger ctxd.Logger = &lm
	)

	// Inspect error fields.
	var se ctxd.StructuredError
	if errors.As(err, &se) {
		fmt.Printf("error fields: %v\n", se.Fields())
	}

	// Log errors.
	ctxd.LogError(ctx, err, logger.Error)
	ctxd.LogError(ctx, err2, logger.Warn)
	fmt.Print(lm.String())

}
Output:

error fields: map[field1:1 field2:abc field3:3]
error: something failed {"field1":1,"field2":"abc","field3":3}
warn: wrapped: something failed {"field1":1,"field2":"abc","field3":3,"field4":true,"field5":"V"}

Types

type DeferredJSON

type DeferredJSON func() interface{}

DeferredJSON postpones log field processing, suitable for debug logging.

func (DeferredJSON) MarshalJSON

func (d DeferredJSON) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

type DeferredString

type DeferredString func() interface{}

DeferredString postpones log field processing, suitable for debug logging.

func (DeferredString) String

func (d DeferredString) String() string

String implements fmt.Stringer.

type FieldNames added in v0.1.4

type FieldNames struct {
	Timestamp string `default:"@timestamp"`
	Message   string `default:"message"`

	// ClientIP is an IP address of the client (IPv4 or IPv6).
	ClientIP string `default:"client.ip"`

	HTTPMethod         string `default:"http.request.method"`
	HTTPResponseBytes  string `default:"http.response.bytes"`
	HTTPResponseStatus string `default:"http.response.status_code"`

	URL string `default:"url.original"`

	// UserAgentOriginal is an unparsed user_agent string.
	UserAgentOriginal string `default:"user_agent.original"`

	SpanID        string `default:"span.id"`
	TraceID       string `default:"trace.id"`
	TransactionID string `default:"transaction.id"`
}

FieldNames defines standard field names.

Default names are aligned with https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html.

type LogFunc

type LogFunc func(ctx context.Context, msg string, keysAndValues ...interface{})

LogFunc defines contextualized logger function.

type Logger

type Logger interface {
	// Debug logs a message.
	Debug(ctx context.Context, msg string, keysAndValues ...interface{})

	// Info logs a message.
	Info(ctx context.Context, msg string, keysAndValues ...interface{})

	// Important forcibly logs an important message with level INFO disregarding logger level constraints.
	// Can be used for logging historically important information.
	Important(ctx context.Context, msg string, keysAndValues ...interface{})

	// Warn logs a message.
	Warn(ctx context.Context, msg string, keysAndValues ...interface{})

	// Error logs a message.
	Error(ctx context.Context, msg string, keysAndValues ...interface{})
}

Logger is a contextualized structured logger.

Logging methods accept keys and values as variadic argument that contains loosely-typed key-value pairs. When processing pairs, the first element of the pair is used as the field key and the second as the field value.

func LoggerWithFields

func LoggerWithFields(logger Logger, keysAndValues ...interface{}) Logger

LoggerWithFields instruments contextualized logger with global fields.

Example
package main

import (
	"context"
	"fmt"

	"github.com/bool64/ctxd"
)

func main() {
	lm := ctxd.LoggerMock{}

	var globalLogger ctxd.Logger = &lm

	localLogger := ctxd.LoggerWithFields(globalLogger, "local", 123)

	ctx1 := ctxd.AddFields(context.Background(),
		"ctx", 1,
		"foo", "bar",
	)
	ctx2 := ctxd.AddFields(context.Background(), "ctx", 2)

	localLogger.Info(ctx1, "hello", "he", "lo")
	localLogger.Warn(ctx2, "bye", "by", "ee")

	fmt.Print(lm.String())

}
Output:

info: hello {"ctx":1,"foo":"bar","he":"lo","local":123}
warn: bye {"by":"ee","ctx":2,"local":123}

type LoggerMock added in v0.1.2

type LoggerMock struct {
	OnError func(err error)

	sync.Mutex
	bytes.Buffer
	LoggedEntries []struct {
		Time    time.Time              `json:"time"`
		Level   string                 `json:"level"`
		Message string                 `json:"message"`
		Data    map[string]interface{} `json:"data,omitempty"`
	}
}

LoggerMock logs messages to internal buffer.

func (*LoggerMock) Debug added in v0.1.2

func (m *LoggerMock) Debug(ctx context.Context, msg string, keysAndValues ...interface{})

Debug logs a message.

func (*LoggerMock) Error added in v0.1.2

func (m *LoggerMock) Error(ctx context.Context, msg string, keysAndValues ...interface{})

Error logs a message.

func (*LoggerMock) Important added in v0.1.2

func (m *LoggerMock) Important(ctx context.Context, msg string, keysAndValues ...interface{})

Important logs a message.

func (*LoggerMock) Info added in v0.1.2

func (m *LoggerMock) Info(ctx context.Context, msg string, keysAndValues ...interface{})

Info logs a message.

func (*LoggerMock) Warn added in v0.1.2

func (m *LoggerMock) Warn(ctx context.Context, msg string, keysAndValues ...interface{})

Warn logs a message.

type LoggerProvider

type LoggerProvider interface {
	CtxdLogger() Logger
}

LoggerProvider is an embeddable provider interface.

type NoOpLogger

type NoOpLogger struct{}

NoOpLogger is a contextualized logger stub.

func (NoOpLogger) CtxdLogger

func (NoOpLogger) CtxdLogger() Logger

CtxdLogger is a provider.

func (NoOpLogger) Debug

func (NoOpLogger) Debug(_ context.Context, _ string, _ ...interface{})

Debug discards debug message.

func (NoOpLogger) Error

func (NoOpLogger) Error(_ context.Context, _ string, _ ...interface{})

Error discards error message.

func (NoOpLogger) Important

func (NoOpLogger) Important(_ context.Context, _ string, _ ...interface{})

Important discards important message.

func (NoOpLogger) Info

func (NoOpLogger) Info(_ context.Context, _ string, _ ...interface{})

Info discards informational message.

func (NoOpLogger) Warn

func (NoOpLogger) Warn(_ context.Context, _ string, _ ...interface{})

Warn discards warning message.

type SentinelError added in v1.1.0

type SentinelError string

SentinelError is a constant error.

See https://dave.cheney.net/2016/04/07/constant-errors for more details.

func (SentinelError) Error added in v1.1.0

func (e SentinelError) Error() string

Error returns error message.

type StructuredError

type StructuredError interface {
	// Error returns message of error.
	Error() string

	// Tuples returns structured data of error in form of loosely-typed key-value pairs.
	Tuples() []interface{}

	// Fields returns structured data of error as a map.
	Fields() map[string]interface{}
}

StructuredError defines error with message and data.

type Tuples

type Tuples []interface{}

Tuples is a slice of keys and values, e.g. {"key1", 1, "key2", "val2"}.

func (Tuples) Fields

func (t Tuples) Fields() map[string]interface{}

Fields creates a map from key-value pairs.

Jump to

Keyboard shortcuts

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