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 ¶
- Variables
- func Into(ctx context.Context, repository *Repository) context.Context
- type ContextLogProcessor
- func (p *ContextLogProcessor) Enabled(ctx context.Context, level slog.Level) bool
- func (p *ContextLogProcessor) Handle(ctx context.Context, record slog.Record) error
- func (p *ContextLogProcessor) WithAttrs(attrs []slog.Attr) slog.Handler
- func (p *ContextLogProcessor) WithGroup(name string) slog.Handler
- type Dispatcher
- type Repository
- func (r *Repository) Add(key any, value ...any) *Repository
- func (r *Repository) AddHidden(key any, value ...any) *Repository
- func (r *Repository) AddHiddenIf(key string, value any) *Repository
- func (r *Repository) AddIf(key string, value any) *Repository
- func (r *Repository) All() map[string]any
- func (r *Repository) AllHidden() map[string]any
- func (r *Repository) Decrement(key string, amount ...int) *Repository
- func (r *Repository) Dehydrate() (map[string]any, error)
- func (r *Repository) Dehydrating(callback func(*Repository)) *Repository
- func (r *Repository) Except(keys []string) map[string]any
- func (r *Repository) ExceptHidden(keys []string) map[string]any
- func (r *Repository) Flush() *Repository
- func (r *Repository) Forget(key ...string) *Repository
- func (r *Repository) ForgetHidden(key ...string) *Repository
- func (r *Repository) Get(key string, def ...any) any
- func (r *Repository) GetHidden(key string, def ...any) any
- func (r *Repository) HandleUnserializeExceptionsUsing(callback func(err error, key string, value any, hidden bool) any) *Repository
- func (r *Repository) Has(key string) bool
- func (r *Repository) HasHidden(key string) bool
- func (r *Repository) HiddenStackContains(key string, value any, strict ...bool) (bool, error)
- func (r *Repository) Hydrate(dehydrated map[string]any) error
- func (r *Repository) Hydrated(callback func(*Repository)) *Repository
- func (r *Repository) Increment(key string, amount ...int) *Repository
- func (r *Repository) IsEmpty() bool
- func (r *Repository) Missing(key string) bool
- func (r *Repository) MissingHidden(key string) bool
- func (r *Repository) Only(keys []string) map[string]any
- func (r *Repository) OnlyHidden(keys []string) map[string]any
- func (r *Repository) Pop(key string) (any, error)
- func (r *Repository) PopHidden(key string) (any, error)
- func (r *Repository) Pull(key string, def ...any) any
- func (r *Repository) PullHidden(key string, def ...any) any
- func (r *Repository) Push(key string, values ...any) (*Repository, error)
- func (r *Repository) PushHidden(key string, values ...any) (*Repository, error)
- func (r *Repository) Remember(key string, value any) any
- func (r *Repository) RememberHidden(key string, value any) any
- func (r *Repository) Scope(callback func() error, data map[string]any, hidden map[string]any) error
- func (r *Repository) StackContains(key string, value any, strict ...bool) (bool, error)
- func (r *Repository) When(condition any, callback func(*Repository), otherwise ...func(*Repository)) *Repository
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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. |