belt

package module
v0.0.0-...-183aff7 Latest Latest
Warning

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

Go to latest
Published: Mar 14, 2024 License: BSD-3-Clause Imports: 5 Imported by: 12

README

go report License

Out of the box tools (interfaces):

Module GoDoc QuickStart
Logger GoDoc example
Metrics (experimental) GoDoc example
Tracer (experimental) GoDoc example
ErrorMonitor (experimental) GoDoc example
Belt GoDoc

Out of the box implementation examples:

Module Implementation GoDoc QuickStart
Logger logrus GoDoc logrus.Default()
Logger zap GoDoc zap.Default()
Logger glog GoDoc glog.New()
Metrics prometheus GoDoc prometheus.Default()
ErrorMonitor sentry GoDoc sentry.New(sentryClient)
Tracer zipkin GoDoc zipkin.New(zipkinTracer)

logo

Index

  1. Mission
  2. About
  3. Overview
  4. Why should I use it?
  5. Quick start
  6. Exotic cases

About

This package contains implementation-agnostic interfaces for application observability (such as logging, metrics and so on) and a collection of implementations of these interfaces. And all the observability tools are merged together into an "observability tool belt".

This project implements these ideas:

  • Dependency injection. No observability tooling should be hardcoded into an application.
  • A tool interface just represents aggregated best practices.
  • Easy to use. Doing application observability comfortable in any project (is it a simple hobby project or a hyperscaler commercial project).
  • Various observability tools share the idea of structured data. Let's take advantage of this.

This crosses with the ideas of OpenTelemetry, but much more focused on "easy to use", contextual structured data, injected dependencies and deeper decoupling. Also OpenTelemetry is more focused on metrics and tracing, while this package pays more attention to logging. These projects does not compete, the opposite: for example, one may implement go-belt interfaces with OpenTelemetry SDK.

Mission

This package intended to improve the culture of application observability in applications written in Go and to standardize approaches used across the community. In other words this is an attempt to accumulate (opinionated but:) the most generic best practices of how to handle observability (and first of all: logging, metrics, tracing and error monitoring).

We do not want to just propose some additional solution. We do want to collect all the best trade-offs together and be open for changes. Please do not hesitate to propose any changes (even the most drastic ones) if you believe that will address this mission. We will try to find the best trade-offs for a generic use case and continuously improve these packages. This is the whole point of the project.

Just in case a reminder: "the best" -- does not mean "perfect", it means "the most practical". Also more drastic the change is, more reasoning it requires.

Overview

There are 5 main components here:

All of these components are generic and abstracted from specific implementation. And some implementations are provided for each of those. For example there are implementations for Logger based on: zap, logrus, glog and standard Go's log package.

And if one needs only the best practices (accumulated so far) for logging, just go to Logger and disregard everything else in here. Pick an existing implementation or write a new one (and create a PR to push it here). All this applies to any other tool.

But if one needs a sane control over multiple tools at the same time then use the Belt. This also applies to tools not mentioned here.

Why should I use it?

  • Provide observability tooling as a dependency injection, instead of hardcoding an implementation. For example the most of IT companies have internal infra with their-specific observability infra -- this package makes code implementation-agnostic, so that any implementation could be injected using the same shared code (see example: ConTest).
  • To make code reusable among different projects. To do not reinvent the same wheels over and over again working in different projects with people of different opinions about observability. In this package we try to cover the most of popular ways to do the logging, hoping it will be a good enough compromise for everybody.
  • To have an application with observability in mind. Even if the application is already implemented without having observability tooling in mind, it is easily fixable by this package. See the Quick start section. Or if one does not plan to add proper observability at a specific moment they still can already start using this package (it does not create essential coding overhead), and in any moment in the future it will be very easy to add all the desired observability.
  • To be aligned with the best practices. It is highly encouraged to constructively question and discuss the approaches applied here. If something is not aligned with the best practices then the goal of this project is to adapt.

Quick start

Logger

See also more detailed info on using Logger in its README.md.

Approach "contextual logger"
import (
	"github.com/facebookincubator/go-belt/belt"
	"github.com/facebookincubator/go-belt/tool/logger/implementation/zap"
)

func main() {
	...
	ctx = logger.CtxWithLogger(ctx, zap.Default())
	...
	someFunc(ctx)
	...
}

func someFunc(ctx context.Context) {
	...
	ctx = belt.WithField(ctx, "user_id", user.ID)
	...
	anotherFunc(ctx)
	...
}

func anotherFunc(ctx context.Context) {
	...
	logger.Debug(ctx, "hello world!") // user_id will also be logged here
	...
}

Also contexts usually are already propagated through a lot of codebases. Thus, one may take advantage of that in an existing codebase.

Approach "safer contextual logger"

There is a major argument against the approach above:

  • context.Context is pretty generic entity and may be generated by anybody, thus there could be lack of guarantee of having Logger in the context (because it is unclear where the specific context came from). This issue is partly mitigated through default Logger and default Belt, but in some cases there could be higher guarantee requirements.

To be sure we work with something correctly setup, it is possible to use Belt directly (instead of using it through a context):

import (
	"github.com/facebookincubator/go-belt"
	"github.com/facebookincubator/go-belt/tool/logger/implementation/zap"
)

func main() {
	...
	belt := belt.New()
	belt = logger.BeltWithLogger(belt, zap.Default())
	...
	someFunc(ctx, belt)
	...
}

func someFunc(ctx context.Context, belt *belt.Belt) {
	...
	belt = belt.WithField("user_id", user.ID)
	...
	anotherFunc(belt)
	...
}

func anotherFunc(ctx context.Context, belt *belt.Belt) {
	...
	logger.FromBelt(belt).Debug("hello world!") // user_id will also be logged here
	...
}

here we always will be sure we work with the entity where Logger is correctly initialized.

Approach "just give me structured logger"

OK-OK. Instead of injecting logger into context, you may just use it directly:

// import "github.com/facebookincubator/go-belt/tool/logger/implementation/zap"

logger := zap.Default()
...
fn(logger) // the function here is agnostic of specific logger implementation

And this logger will be based on Uber's zap.

Approach "standard global logger"
// import "github.com/facebookincubator/go-belt/tool/logger/implementation/stdlib"

stdlib.Default().Debug("Hello world!")

Even though this approach is discouraged, it still keeps possibility to get proper structured logging and all the fancy stuff at any moment without any difficult changes in the code.

Metrics

Let's say we used the "contextual logger" approach for logging, and now we want to add metrics. To do so we need only to add something like this to the initialization code:

// import (
// 	promadapter "github.com/facebookincubator/go-belt/tool/experimental/metrics/implementation/prometheus"
// 	"github.com/prometheus/client_golang/prometheus"
// )
promRegistry := prometheus.NewRegistry()
ctx = metrics.CtxWithMetrics(ctx, promadapter.New(promRegistry))

and that's it. Now you can use prometheus metrics, for example:

metrics.FromCtx(ctx).Count("requests").Add(1)

It will also include all the structured fields (added for example with WithField) allowed for metrics as labels for the prometheus metric. For example:

import "github.com/facebookincubator/go-belt/tool/experimental/metrics"

func someFunc(ctx context.Context) {
	...
	ctx = belt.WithField(ctx, "user_id", user.ID, metrics.FieldPropInclude)
	...
	processRequest(ctx, req)
	...
}

func processRequest(ctx context.Context, req Request) {
	defer metrics.FromCtx(ctx).GaugeInt("concurrent_request").Add(1).Add(-1) // "user_id" will be used here as a prometheus label.
	...

	logger.FromCtx(ctx).Debug("hello world!") // and also "user_id" will be logged here as well.
}

It is required to add metrics.FieldPropInclude for fields which are used for metrics, because the amount of actual metrics proportional to the multiplication of all used values in all the labels. And some structured fields may be pretty random causing to generate unlimited amount of metrics and consume all the memory.

Error monitor

Now let's organize application errors. It is doable through just something like:

	// import "github.com/facebookincubator/go-belt/tool/experimental/errmon/implementation/sentry"
	ctx = errmon.CtxWithErrorMonitor(ctx, sentry.New(sentryClient))

in the initialization code, and then using the error monitor where it is required. For example:

func someFunc(ctx context.Context) {
	defer func(){ errmon.ObserveRecoverCtx(ctx, recover()) }()

	...
	_, err := writer.Write(b)
	errmon.ObserveErrorCtx(ctx, err)
	...
}

Specifically this code will send to Sentry all the errors observed from Write and panics in someFunc.

Again, all the fields (for example added through WithField) will also be logged in a structured way as part of the event.

Other features (like Breadcrumb-s) are also supported. For example:

ctx = belt.WithField("breadcrumb_user.fetch", &errmon.Breadcrumb{
	TS:         time.Now()
	Path:       []string{"user", "fetch"}
	Categories: []string{"user", "network"}
	Data:       fetchUserErr,
})

And:

errmon.ObserveErrorCtx(ctx, err)

now will also send the breadcrumb to the Sentry (or another error monitor implementation injected).

Distributed tracing

Again, first initializing it:

	ctx = tracer.CtxWithTracer(ctx, zipkinadapter.New(zipkinClient))

And use it:

func mysqlQuery(ctx context.Context, query string, args ...any) {
	span, ctx := tracer.StartChildSpanFromCtx(ctx, "MySQL-query")
	defer span.Finish()
	// ..do the MySQL query here..
}

That's it. Of course there are more features to cover the most generic needs.

Other tooling

Other observability tooling could be easily introduced into the Belt. Actually any of logger, metrics, tracer and errmon could have been provided by external projects and it would have work absolutely the same. In other words one may use these examples to create theirown standardized observability tooling and it will just work. And if you believe you created a good example of an observability tool then feel free to make a Pull Request to add it to tool-s here.

More examples

See examples directory.

Performance

The package is pretty much high-performance-aware despite being generic. For example using of logrus through this package makes it multiple times FASTER than using it directly. It makes even zap somewhat faster in a non-noop cases. This happens due to another design of handling fields, which avoids a lot of computation duplication for structured fields and provide already compiled (in a faster way) structures to the backend logger. For example on some synthetic tests it makes zap 15 times faster:

Benchmark/prod/depth205/WithField/callLog-false/bare_zap-16       	     128	    914923 ns/op	 4883118 B/op	    3286 allocs/op
Benchmark/prod/depth205/WithField/callLog-false/adapted_zap-16    	    1870	     60388 ns/op	  103320 B/op	    1845 allocs/op
Benchmark/prod/depth205/WithField/callLog-true/bare_zap-16        	     126	    901277 ns/op	 4886755 B/op	    3290 allocs/op
Benchmark/prod/depth205/WithField/callLog-true/adapted_zap-16     	     840	    141649 ns/op	  247172 B/op	    1866 allocs/op

Exotic cases

Type-assertion

It is still allowed (though discouraged) to do type-assertion of an observability tool if it is necessary. For example:

logrusEntry := logger.GetEmitter(ctx).(*logrusadapter.Emitter).LogrusEntry
logrusEntry = logrusEntry.WithFields(logrus.Fields{
	"oldFashionLogrusFieldKey": "some value",
})
logrusEntry.Debugf("hey!")

Writing a custom Logger

Depending on how many features your logger is ready to provide it should implement one of:

And then you may call adapter.LoggerFromAny and it will convert your logger to Logger by adding everything what is missing in a naive generic way.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Default = func() *Belt {
	return New()
}

Default returns an Belt with the default configuration and state.

This function is overridable. Please override it only and only from a `main` package.

Functions

func CtxWithBelt

func CtxWithBelt(ctx context.Context, belt *Belt) context.Context

CtxWithBelt returns a context derivative which includes the Belt as a value.

func Flush

func Flush(ctx context.Context)

CtxFlush forces to flush all buffers of all the tools.

func GetFields

func GetFields(ctx context.Context) field.AbstractFields

GetFields returns returns the set of fields set in the scope of this Belt.

Do not modify the output of this function! It is for reading only.

func WithArtifact

func WithArtifact(ctx context.Context, artifactID ArtifactID, artifact Artifact) context.Context

WithArtifact returns a derivative of the context, but with the Artifact set.

func WithField

func WithField(ctx context.Context, key string, value interface{}, props ...field.Property) context.Context

WithField returns a context with a clone/derivative of the Belt which includes the passed value.

The value is used by observability tooling. For example a Logger derived from the resulting Belt may add this value to the structured fields of each log entry.

func WithFields

func WithFields(ctx context.Context, fields field.AbstractFields) context.Context

WithFields is the same as WithField, but adds multiple Fields at the same time.

It is more performance efficient than adding fields by one.

func WithMap

func WithMap(ctx context.Context, m map[string]interface{}, props ...field.Property) context.Context

WithMap is just a sugar method, which provides logrus like way of adding fields. Effectively the same as WithFields, just the argument are in another format.

func WithTool

func WithTool(ctx context.Context, toolID ToolID, tool Tool) context.Context

WithTool returns a context with an Belt clone/derivative, but the provided tool added to the collection of tools.

Special case: to remove a specific tool, just passed an untyped nil as `tool`.

func WithTraceID

func WithTraceID(ctx context.Context, traceIDs ...TraceID) context.Context

WithTraceID returns a context with an Belt clone/derivative with the passed traceIDs added to the set of TraceIDs.

Types

type Artifact

type Artifact any

Artifact is a specific object attached to current context by any of the observability tools.

type ArtifactID

type ArtifactID any

ArtifactID is an unique ID of an Artifact.

An example of such objects is a Span left by a Tracer.

type Artifacts

type Artifacts map[ArtifactID]Artifact

Artifacts is a set of Artifact-s.

func GetArtifacts

func GetArtifacts(ctx context.Context) Artifacts

GetArtifacts returns the collection of Artifacts in the scope of the Belt.

Do not modify the output of this function! It is for reading only.

func (Artifacts) GetByID

func (artifacts Artifacts) GetByID(artifactID ArtifactID) Artifact

GetByID returns an Artifact given its ID.

Returns an untyped nil if an Artifact is not set.

type Belt

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

Belt (tool belt for observability) is the handler which orchestrates all the observability tooling.

Things like Logger, Tracer, Metrics, ErrorMonitoring are handled together, and are available at any moment, given an Belt.

For example one may call `logger.FromBelt(belt)` to get a `Logger`, if `belt` is an `Belt`. It is different from just storing this tooling as values within a context, for example it is: * More performance efficient. * Safer (e.g. you won't get an untyped nil because a context was accidentally changed to a new one) * More convenient: no need to manage each tool separately. And all the possible sugar is already in place. * Reusable. It is a generic standardized way to handle observability tooling. * Quality. Here we try to accumulate best practices, instead of having a quick solution for a single project.

func CtxBelt

func CtxBelt(ctx context.Context) *Belt

CtxBelt returns the Belt from context values. Returns the default observer if one is not set in the context.

func New

func New() *Belt

New returns a new instance of an Belt

func (*Belt) Artifacts

func (belt *Belt) Artifacts() Artifacts

Artifacts returns the collection of Artifacts in the scope of the Belt.

Do not modify the output of this function! It is for reading only.

func (*Belt) Fields

func (belt *Belt) Fields() field.AbstractFields

Fields returns returns the set of fields set in the scope of this Belt.

Do not modify the output of this function! It is for reading only.

func (*Belt) Flush

func (belt *Belt) Flush()

Flush forces to flush all buffers of all the tools.

func (*Belt) Tools

func (belt *Belt) Tools() Tools

Tools returns the current collection of Tools.

Do not modify the output of this function! It is for reading only.

func (*Belt) TraceIDs

func (belt *Belt) TraceIDs() TraceIDs

TraceIDs returns the current set of TraceID-s.

Do not modify the output of this function! It is for reading only.

func (*Belt) WithArtifact

func (belt *Belt) WithArtifact(artifactID ArtifactID, artifact Artifact) *Belt

WithArtifact returns a clone of the Belt, but with the Artifact set.

func (*Belt) WithField

func (belt *Belt) WithField(key string, value field.Value, props ...field.Property) *Belt

WithField returns a clone/derivative of the Belt which includes the passed value.

The value is used by observability tooling. For example a Logger derived from the resulting Belt may add this value to the structured fields of each log entry.

func (*Belt) WithFields

func (belt *Belt) WithFields(fields field.AbstractFields) *Belt

WithFields is the same as WithField, but adds multiple Fields at the same time.

It is more performance efficient than adding fields by one.

func (*Belt) WithMap

func (belt *Belt) WithMap(m map[string]interface{}, props ...field.Property) *Belt

WithMap is just a sugar method, which provides logrus like way of adding fields. Effectively the same as WithFields, just the argument are in another format.

func (*Belt) WithTool

func (belt *Belt) WithTool(toolID ToolID, tool Tool) *Belt

WithTool returns an Belt clone/derivative, but the provided tool added to the collection of tools.

Special case: to remove a specific tool, just passed an untyped nil as `tool`.

func (*Belt) WithTraceID

func (belt *Belt) WithTraceID(traceIDs ...TraceID) *Belt

WithTraceID returns an Belt clone/derivative with the passed traceIDs added to the set of TraceIDs.

type Tool

type Tool interface {
	// Flush forces to flush all buffers.
	Flush()

	// WithContextFields sets new context-defined fields. Supposed to be called
	// only by an Belt.
	//
	// allFields contains all fields as a chain of additions in a reverse-chronological order,
	// while newFieldsCount tells about how much of the fields are new (since last
	// call of WithContextFields). Thus if one will call
	// field.Slice(allFields, 0, newFieldsCount) they will get only the new fields.
	// At the same time some Tool-s may prefer just to re-set all the fields instead of adding
	// only new fields (due to performance reasons) and they may just use `allFields`.
	WithContextFields(allFields *field.FieldsChain, newFieldsCount int) Tool

	// WithTraceIDs sets new context-defined TraceIDs. Supposed to be called
	// only by an Belt.
	//
	// traceIDs and newTraceIDsCount has similar properties as allFields and newFieldsCount
	// in the WithContextFields method.
	WithTraceIDs(traceIDs TraceIDs, newTraceIDsCount int) Tool
}

Tool is an abstract observability tool. It could be a Logger, metrics, tracing or anything else.

type ToolID

type ToolID interface{}

ToolID is an unique identifier of a Tool. A ToolID should be always defined using a private-type, so that no other package can define the same ToolID.

type Tools

type Tools map[ToolID]Tool

Tools is a collection of observability Tool-s.

func GetTools

func GetTools(ctx context.Context) Tools

GetTools returns the current collection of Tools.

Do not modify the output of this function! It is for reading only.

func (Tools) GetByID

func (tools Tools) GetByID(toolID ToolID) Tool

GetByID returns a Tool of a specified ID. Returns an untyped nil if such Tool is not set.

type TraceID

type TraceID string

TraceID is a passthrough ID used to track a sequence of events across multiple processes/services.

func RandomTraceID

func RandomTraceID() TraceID

RandomTraceID returns a new TraceID with a (random) UUIDv4 value.

type TraceIDs

type TraceIDs []TraceID

TraceIDs is a set of TraceID-s.

func GetTraceIDs

func GetTraceIDs(ctx context.Context) TraceIDs

GetTraceIDs returns the current set of TraceID-s.

Do not modify the output of this function! It is for reading only.

Jump to

Keyboard shortcuts

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