logging

package
v0.296.0 Latest Latest
Warning

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

Go to latest
Published: Apr 9, 2025 License: Apache-2.0 Imports: 18 Imported by: 3

README

package logging

Package logging provides tooling for structured logging. With logging, you can use context to add logging details to your call stack.

How to use

package main

import (
	"context"

	"go.llib.dev/frameless/pkg/logging"
)

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

	// You can add details to the context; thus, every logging call using this context will inherit the details.
	ctx = logging.ContextWith(ctx, logging.Fields{
		"foo": "bar",
		"baz": "qux",
	})

	var l logging.Logger // uses the defaults

	// You can use your own Logger instance or the logger.Default logger instance if you plan to log to the STDOUT. 
	l.Info(ctx, "foo", logging.Fields{
		"userID":    42,
		"accountID": 24,
	})
}

example output when snake case key format is defined (default):

{
  "account_id": 24,
  "baz": "qux",
  "foo": "bar",
  "level": "info",
  "message": "foo",
  "timestamp": "2023-04-02T16:00:00+02:00",
  "user_id": 42
}

example output when camel key format is defined:

{
  "accountID": 24,
  "baz": "qux",
  "foo": "bar",
  "level": "info",
  "message": "foo",
  "timestamp": "2023-04-02T16:00:00+02:00",
  "userID": 42
}

How to configure:

logging.Logger can be configured through its struct fields; please see the documentation for more details. To configure the default logger, simply configure it from your main.

package main

import "go.llib.dev/frameless/pkg/logging"

func main() {
	var l logging.Logger
	l.MessageKey = "msg"
	l.TimestampKey = "ts"
}

Key string case style consistency in your logs

Using the logger package can help you maintain historical consistency in your logging key style and avoid mixed string case styles. This is particularly useful since many log collecting systems rely on an append-only strategy, and any inconsistency could potentially cause issues with your alerting scripts that rely on certain keys. So, by using the logger package, you can ensure that your logging is clean and consistent, making it easier to manage and troubleshoot any issues that may arise.

The default key format is snake_case, but you can change it easily by supplying a formetter in the KeyFormatter Logger field.

package main

import (
	"go.llib.dev/frameless/pkg/logging"
	"go.llib.dev/frameless/pkg/stringkit"
)

func main() {
	var l logging.Logger
	l.KeyFormatter = stringkit.ToKebab
}

Security concerns

It is crucial to make conscious decisions about what we log from a security standpoint because logging too much information can potentially expose sensitive data about users. On the other hand, by logging what is necessary and relevant for operations, we can avoid security and compliance issues. Following this practice can reduce the attack surface of our system and limit the potential impact of a security breach. Additionally, logging too much information can make detecting and responding to security incidents more difficult, as the noise of unnecessary data can obscure the signal of actual threats.

The logger package has a safety mechanism to prevent exposure of sensitive data; it requires you to register any DTO or Entity struct type with the logging package before you can use it as a logging field.

package mydomain

import "go.llib.dev/frameless/pkg/logger"

type MyEntity struct {
	ID               string
	NonSensitiveData string
	SensitiveData    string
}

var _ = logger.RegisterFieldType(func(ent MyEntity) logging.LoggingDetail {
	return logging.Fields{
		"id":   ent.ID,
		"data": ent.NonSensitiveData,
	}
})

ENV based Configuration

You can set the default's loggers level with the following env values:

  • LOG_LEVEL
  • LOGGER_LEVEL
  • LOGGING_LEVEL
logging level env value env short format value
Debug Level debug d
Info Level info i
Warn Level warn w
Error Level error e
Fatal Level fatal f
Fatal Level critical c

Logging performance optimisation by using an async logging strategy

If your application requires high performance and uses a lot of concurrent actions, using an async logging strategy can provide the best of both worlds. This means the application can have great performance that is not hindered by logging calls, while also being able to observe and monitor its behavior effectively.

----------------- no concurrency heavy concurrency
sync logging 5550 ns/op 54930 ns/op
async logging 700.7 ns/op 1121 ns/op

tested on MacBook Pro with M1 when writing into a file

$ go test -bench BenchmarkLogger_AsyncLogging -run -

package main

import (
	"go.llib.dev/frameless/pkg/logger"
)

func main() {
	var l logging.Logger
	defer l.AsyncLogging()() // from here on, logging will be async
	l.Info(nil, "this is logged asynchronously")
}

Documentation

Overview

Package logger provides tooling for structured logging. With logger, you can use context to add logging details to your call stack.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ContextWith

func ContextWith(ctx context.Context, lds ...Detail) context.Context
Example
package main

import (
	"context"

	"go.llib.dev/frameless/pkg/logging"
)

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

	ctx = logging.ContextWith(ctx, logging.Fields{
		"foo": "bar",
		"baz": "qux",
	})

	l := &logging.Logger{}
	l.Info(ctx, "message") // will have details from the context
}
Output:

func RegisterFieldType

func RegisterFieldType[T any](mapping func(T) Detail) func()

RegisterFieldType allows you to register T type and have it automatically conerted into a Detail value when it is passed as a Field value for logging.

Example (AsLoggingDetails)
package main

import (
	"go.llib.dev/frameless/pkg/logging"
)

func main() {
	type MyEntity struct {
		ID               string
		NonSensitiveData string
		SensitiveData    string
	}

	// at package level
	var _ = logging.RegisterFieldType(func(ent MyEntity) logging.Detail {
		return logging.Fields{
			"id":   ent.ID,
			"data": ent.NonSensitiveData,
		}
	})
}
Output:

func Stub

func Stub(tb testingTB) (*Logger, StubOutput)

Stub the logger.Default and return the buffer where the logging output will be recorded. Stub will restore the logger.Default after the test.

Types

type Detail

type Detail interface {
	// contains filtered or unexported methods
}

Detail is a logging detail that enrich the logging message with additional contextual detail.

func ErrField

func ErrField(err error) Detail
Example
package main

import (
	"context"
	"errors"

	"go.llib.dev/frameless/pkg/logging"
)

func main() {
	ctx := context.Background()
	err := errors.New("boom")
	var l logging.Logger

	l.Error(ctx, "task failed successfully", logging.ErrField(err))
}
Output:

func Field

func Field(key string, value any) Detail

Field creates a single key value pair based logging detail. It will enrich the log entry with a value in the key you gave.

Example
package main

import (
	"context"

	"go.llib.dev/frameless/pkg/logging"
)

func main() {
	var l logging.Logger

	l.Error(context.Background(), "msg",
		logging.Field("key1", "value"),
		logging.Field("key2", "value"))
}
Output:

type Fields

type Fields map[string]any

Fields is a collection of field that you can add to your loggig record. It will enrich the log entry with a value in the key you gave.

Example
package main

import (
	"context"

	"go.llib.dev/frameless/pkg/logging"
)

func main() {
	var l logging.Logger
	l.Error(context.Background(), "msg", logging.Fields{
		"key1": "value",
		"key2": "value",
	})
}
Output:

type HijackFunc

type HijackFunc func(level Level, msg string, fields Fields)

type LazyDetail added in v0.239.0

type LazyDetail func() Detail

LazyDetail lets you add logging details that aren’t evaluated until the log is actually created. This is useful when you want to add fields to a debug log that take effort to calculate, but would be skipped in a production environment because of the logging level.

Example
package main

import (
	"context"

	"go.llib.dev/frameless/pkg/logging"
)

func main() {
	var l logging.Logger

	l.Debug(context.Background(), "msg", logging.LazyDetail(func() logging.Detail {
		// only runs if the logging level is enabled and the logging details are collected
		return logging.Field("key1", "value")
	}))
}
Output:

type Level

type Level string
const (
	LevelDebug Level = "debug"
	LevelInfo  Level = "info"
	LevelWarn  Level = "warn"
	LevelError Level = "error"
	LevelFatal Level = "fatal"
)

func (Level) Can added in v0.218.1

func (l Level) Can(oth Level) bool

func (Level) Less added in v0.218.1

func (l Level) Less(oth Level) bool

func (Level) String

func (l Level) String() string

type Logger

type Logger struct {
	Out io.Writer

	// Level is the logging level.
	// The default Level is LevelInfo.
	Level Level

	Separator string

	MessageKey   string
	LevelKey     string
	TimestampKey string

	// MarshalFunc is used to serialise the logging message event.
	// When nil it defaults to JSON format.
	MarshalFunc func(any) ([]byte, error)
	// KeyFormatter will be used to format the logging field keys
	KeyFormatter func(string) string

	// Hijack will hijack the logging and instead of letting it logged out to the Out,
	// the logging will be done with the Hijack function.
	// This is useful if you want to use your own choice of logging,
	// but also packages that use this logging package.
	Hijack HijackFunc

	TestingTB testingTB

	// FlushTimeout is a deadline time for async logging to tell how much time it should wait with batching.
	//
	// Default: 1s
	FlushTimeout time.Duration
	// contains filtered or unexported fields
}

func (*Logger) AsyncLogging

func (l *Logger) AsyncLogging() func()

AsyncLogging will change the logging strategy from sync to async. This makes the log calls such as Logger.Info not wait on io operations. The AsyncLogging function call is where the async processing will happen, You should either run it in a separate goroutine, or use it with the tasker package. After the AsyncLogging returned, the logger returns to log synchronously.

Example
package main

import (
	"context"

	"go.llib.dev/frameless/pkg/logging"
)

func main() {
	ctx := context.Background()
	l := logging.Logger{}
	defer l.AsyncLogging()()
	l.Info(ctx, "this log message is written out asynchronously")
}
Output:

func (*Logger) Clone

func (l *Logger) Clone() *Logger

func (*Logger) Debug

func (l *Logger) Debug(ctx context.Context, msg string, ds ...Detail)

func (*Logger) Error

func (l *Logger) Error(ctx context.Context, msg string, ds ...Detail)

func (*Logger) Fatal

func (l *Logger) Fatal(ctx context.Context, msg string, ds ...Detail)

func (*Logger) Info

func (l *Logger) Info(ctx context.Context, msg string, ds ...Detail)

func (*Logger) Warn

func (l *Logger) Warn(ctx context.Context, msg string, ds ...Detail)

type StubOutput

type StubOutput interface {
	io.Reader
	String() string
	Bytes() []byte
}

Jump to

Keyboard shortcuts

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