context

package
v0.29.0 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package context is the log context that crosses a whole request: a Repository carried on the context.Context, and the handler that copies it onto every log line.

What this is

Repository is the context that crosses a whole request and lands in every log line it produces. Add a value once, at the edge, and every line after it carries the value without any call site passing it down:

ctx = context.Into(ctx, context.New(dispatcher))
context.For(ctx).Add("order_id", order.ID)

It has a visible half and a hidden half. The visible half reaches the log line, through ContextLogProcessor. The hidden half never does -- it exists so that something can travel with a queued job without being printed.

Where the repository lives

The carrier is the context.Context: Into puts a repository in, For reads it back. There is no process-wide repository, because a Go server runs every request in the same process at the same time, and one shared repository would be one request reading another request's context.

Every method is safe on a nil receiver, so a caller that got a repository out of a context that carries none does not have to check: reading gives the zero value and writing goes nowhere.

Errors

Eight methods can fail, and they return an error rather than panicking: Push, Pop, PushHidden, PopHidden, StackContains, HiddenStackContains, Dehydrate and Hydrate. ErrUnableToPush, ErrUnableToPop and ErrNotAStack are the three sentinels the stack failures wrap.

Queued jobs

Nothing here hooks itself into a queue. A queue integration calls the two halves itself: on the way out, Dehydrate and attach the result to the job payload; on the way in, Hydrate from it.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnableToPush is what Push and PushHidden report for a key that holds
	// something other than a list.
	ErrUnableToPush = errors.New("log/context: unable to push value onto context stack")

	// ErrUnableToPop is what Pop and PopHidden report for a key that is not a
	// stack or is an empty one.
	ErrUnableToPop = errors.New("log/context: unable to pop value from context stack")

	// ErrNotAStack is what StackContains and HiddenStackContains report for a
	// key that holds something other than a list.
	ErrNotAStack = errors.New("log/context: key is not a stack")
)

The three stack failures. Each error names the key that caused it and wraps the sentinel, so that a caller can tell the three apart with errors.Is instead of reading the text.

Functions

func Into

func Into(ctx context.Context, repository *Repository) context.Context

Into stores the repository in ctx and returns the new context.

The context is the carrier because one repository shared across the process is not safe under concurrency, and one per context is.

Types

type ContextLogProcessor

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

ContextLogProcessor is the handler that copies the context repository's visible entries onto every record it writes, which is what makes a value added once, at the edge, show up in every log line of the request without anybody passing it down.

It is a slog.Handler wrapping another one rather than a func, because slog has no processor hook -- a wrapping handler is where slog puts "run before the next one sees the record".

Hidden entries are not copied. That is the whole point of hidden: it travels with the request and into a queued job, and it does not land in the log.

func NewContextLogProcessor

func NewContextLogProcessor(next slog.Handler) *ContextLogProcessor

NewContextLogProcessor wraps a handler so that the context repository's visible entries reach every record it writes.

func (*ContextLogProcessor) Enabled

func (p *ContextLogProcessor) Enabled(ctx context.Context, level slog.Level) bool

Enabled reports whether the wrapped handler wants this level. It is asked before the attributes are gathered, so a line below the level costs nothing.

func (*ContextLogProcessor) Handle

func (p *ContextLogProcessor) Handle(ctx context.Context, record slog.Record) error

Handle copies the visible context onto the record and passes it on.

The context repository is read from the ctx, which is where a request-scoped value lives.

slog has one flat set of attributes, so a repeated name is a real collision. An entry whose name a caller already used is not overwritten: what the caller said about this one line beats what the request said about all of them.

The two ways a caller speaks a name

There are two, and only one of them used to be looked at. A name passed to the log call is on the record; a name passed to .With is on the handler, and never reaches the record at all. So a logger derived as .With("order_id", "B"), logging under a repository holding order_id C, produced {"order_id":"C","order_id":"B"} -- a duplicate key, which JSON allows to be written and no two parsers agree on. The same collision on the record resolved the other way and dropped the repository's value, so the two spellings of the same rule disagreed with each other. claimed is what closes it: WithAttrs records the names it was handed, and both spellings now leave the caller's value standing.

WithGroup opens a namespace, and everything after it -- the derived attributes, the record's own, and the ones added here -- is nested inside it together. So the claimed set starts empty again there, and a name claimed outside the group does not stop the repository reaching into it.

func (*ContextLogProcessor) WithAttrs

func (p *ContextLogProcessor) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs answers slog.Handler.WithAttrs, keeping the wrapper in place so that a logger derived with .With still carries the context.

It remembers the names it was handed, because they are the ones ContextLogProcessor.Handle can no longer find by walking the record.

func (*ContextLogProcessor) WithGroup

func (p *ContextLogProcessor) WithGroup(name string) slog.Handler

WithGroup answers slog.Handler.WithGroup, keeping the wrapper in place for the same reason.

The claimed set does not travel through it: a group is a namespace, and a name claimed outside it is not the name it holds.

type Dispatcher

type Dispatcher interface {
	// Dispatch fires the event.
	Dispatch(event any)

	// Listen registers a listener that receives every event; Dehydrating and
	// Hydrated wrap it with the type assertion that selects theirs.
	Listen(listener func(event any))
}

Dispatcher is the slice of an event dispatcher this package needs, declared on the side that consumes it so that one concrete dispatcher can serve every package that fires an event.

type Repository

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

Repository is the context that crosses a whole request and lands in every log line it produces.

A Repository is safe for concurrent use.

func For

func For(ctx context.Context) *Repository

For returns the repository ctx carries, or nil when it carries none.

It is the read side of Into. Every method is safe on a nil receiver, so a caller may use the result without checking: reading gives the zero value and writing goes nowhere.

func New

func New(dispatcher Dispatcher) *Repository

New returns an empty repository that fires its events on dispatcher.

The dispatcher may be nil: a repository without one still holds context, and Dehydrating, Hydrated, Dehydrate and Hydrate simply have nobody to tell.

func (*Repository) Add

func (r *Repository) Add(key any, value ...any) *Repository

Add adds a context value.

key is any because it takes two shapes: a string with a value, or a map[string]any on its own, whose entries are merged in. A string key with no value adds nil.

func (*Repository) AddHidden

func (r *Repository) AddHidden(key any, value ...any) *Repository

AddHidden is Add into the hidden half, which is the half that travels with a queued job but never reaches a log line.

func (*Repository) AddHiddenIf

func (r *Repository) AddHiddenIf(key string, value any) *Repository

AddHiddenIf is AddIf over the hidden half.

func (*Repository) AddIf

func (r *Repository) AddIf(key string, value any) *Repository

AddIf adds the value only when the key is not there yet.

func (*Repository) All

func (r *Repository) All() map[string]any

All returns all the context data.

It is a copy, because a live map would be read while another goroutine writes it.

func (*Repository) AllHidden

func (r *Repository) AllHidden() map[string]any

AllHidden returns a copy of the hidden half.

func (*Repository) Decrement

func (r *Repository) Decrement(key string, amount ...int) *Repository

Decrement is Increment with the amount negated.

func (*Repository) Dehydrate

func (r *Repository) Dehydrate() (map[string]any, error)

Dehydrate writes the context down so it can travel with a queued job.

It dispatches ContextDehydrating with a copy first, so a listener can still change what travels, and returns nil when there is nothing to carry, which is the signal that no context needs to be attached.

Each value is encoded as JSON, because that is the encoding that survives crossing into a process built from different code.

func (*Repository) Dehydrating

func (r *Repository) Dehydrating(callback func(*Repository)) *Repository

Dehydrating runs the callback when the context is about to be written down for a queued job.

The callback receives the repository the event carries, which is the copy made for the dehydration, not this one. Without a dispatcher there is nothing to listen on and the call does nothing.

func (*Repository) Except

func (r *Repository) Except(keys []string) map[string]any

Except returns everything but the given keys.

func (*Repository) ExceptHidden

func (r *Repository) ExceptHidden(keys []string) map[string]any

ExceptHidden is Except over the hidden half.

func (*Repository) Flush

func (r *Repository) Flush() *Repository

Flush drops everything, both halves.

func (*Repository) Forget

func (r *Repository) Forget(key ...string) *Repository

Forget drops the keys.

A key that is not there is not an error.

func (*Repository) ForgetHidden

func (r *Repository) ForgetHidden(key ...string) *Repository

ForgetHidden is Forget over the hidden half.

func (*Repository) Get

func (r *Repository) Get(key string, def ...any) any

Get returns the key's value, or the default the variadic carries.

A func() any default is called only when it is needed. A key holding nil takes the default too: Has is what tells a missing key from a nil one.

func (*Repository) GetHidden

func (r *Repository) GetHidden(key string, def ...any) any

GetHidden is Get over the hidden half.

func (*Repository) HandleUnserializeExceptionsUsing

func (r *Repository) HandleUnserializeExceptionsUsing(callback func(err error, key string, value any, hidden bool) any) *Repository

HandleUnserializeExceptionsUsing sets what to do with a value Hydrate cannot read back.

It is package level, so it is set once for the process and not per repository. The callback returns the value to use in place of the broken one; a nil callback restores the default, which is to fail.

func (*Repository) Has

func (r *Repository) Has(key string) bool

Has reports whether the key exists.

A key added with a nil value is here, even though Get returns the default for it: presence and value are two different questions.

func (*Repository) HasHidden

func (r *Repository) HasHidden(key string) bool

HasHidden reports whether the key exists in the hidden half.

func (*Repository) HiddenStackContains

func (r *Repository) HiddenStackContains(key string, value any, strict ...bool) (bool, error)

HiddenStackContains is StackContains over the hidden half.

func (*Repository) Hydrate

func (r *Repository) Hydrate(dehydrated map[string]any) error

Hydrate reads a dehydrated context back, replacing whatever this repository held.

It accepts what Dehydrate produced, and nil, which leaves an empty context. A value that will not read back goes to the callback HandleUnserializeExceptionsUsing set, and without one it is returned as an error.

func (*Repository) Hydrated

func (r *Repository) Hydrated(callback func(*Repository)) *Repository

Hydrated runs the callback once a dehydrated context has been read back.

func (*Repository) Increment

func (r *Repository) Increment(key string, amount ...int) *Repository

Increment adds to a counter, starting it at zero.

The variadic is the amount, and it defaults to one. A key holding something that is not a number restarts from zero.

func (*Repository) IsEmpty

func (r *Repository) IsEmpty() bool

IsEmpty reports whether there is nothing visible and nothing hidden.

func (*Repository) Missing

func (r *Repository) Missing(key string) bool

Missing reports whether the key is absent.

func (*Repository) MissingHidden

func (r *Repository) MissingHidden(key string) bool

MissingHidden reports whether the key is absent from the hidden half.

func (*Repository) Only

func (r *Repository) Only(keys []string) map[string]any

Only returns the values of the given keys, and nothing else.

A key that is not there is simply absent from the result.

func (*Repository) OnlyHidden

func (r *Repository) OnlyHidden(keys []string) map[string]any

OnlyHidden is Only over the hidden half.

func (*Repository) Pop

func (r *Repository) Pop(key string) (any, error)

Pop takes the latest value off the key's stack.

A key that is not a stack, and a stack that is empty, are both an error matching ErrUnableToPop.

func (*Repository) PopHidden

func (r *Repository) PopHidden(key string) (any, error)

PopHidden is Pop over the hidden half.

func (*Repository) Pull

func (r *Repository) Pull(key string, def ...any) any

Pull returns the key's value, and then forgets the key.

func (*Repository) PullHidden

func (r *Repository) PullHidden(key string, def ...any) any

PullHidden is Pull over the hidden half.

func (*Repository) Push

func (r *Repository) Push(key string, values ...any) (*Repository, error)

Push appends the values to the key's stack.

A key that holds something which is not a list is not stackable, and the error for it matches ErrUnableToPush. A key that holds nothing is stackable, and pushing creates the stack.

func (*Repository) PushHidden

func (r *Repository) PushHidden(key string, values ...any) (*Repository, error)

PushHidden is Push over the hidden half.

func (*Repository) Remember

func (r *Repository) Remember(key string, value any) any

Remember adds the value when the key is missing, and returns the value either way.

A func() any value is only called when the key is missing.

func (*Repository) RememberHidden

func (r *Repository) RememberHidden(key string, value any) any

RememberHidden is Remember over the hidden half.

func (*Repository) Scope

func (r *Repository) Scope(callback func() error, data map[string]any, hidden map[string]any) error

Scope runs the callback with the given values added, and puts the context back the way it was afterwards -- including when the callback fails or panics.

A Go method cannot take a type parameter, so the callback returns only an error and a caller who wants a value closes over it.

func (*Repository) StackContains

func (r *Repository) StackContains(key string, value any, strict ...bool) (bool, error)

StackContains reports whether the value is in the key's stack.

A key that is not a stack is an error matching ErrNotAStack; a key that holds nothing is a stack, and the answer is false. value may be a func(any) bool, which is then the test each item is put to. The variadic is strictness: strict compares type and value, loose compares two numbers numerically and anything else by its printed form.

func (*Repository) When

func (r *Repository) When(condition any, callback func(*Repository), otherwise ...func(*Repository)) *Repository

When runs the callback when the condition holds, and the fallback when it does not.

condition is a bool, a func() bool, a func(*Repository) bool, or any value read for truthiness. The variadic is the fallback.

Directories

Path Synopsis
Package events is the two events the log context fires: ContextDehydrating, when the context is about to be written down for a queued job, and ContextHydrated, once it has been read back.
Package events is the two events the log context fires: ContextDehydrating, when the context is about to be written down for a queued job, and ContextHydrated, once it has been read back.

Jump to

Keyboard shortcuts

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