Documentation
¶
Overview ¶
Package log is the logger, the log channels and the development-time Collector, and it is the reason this framework exists.
slog covers structured logging and OpenTelemetry covers production tracing. What is missing between the two is the development layer: the moment a request broke and you want the stack, the queries with their timing, what you dumped and what the framework thinks is wrong, on one screen.
Logging ¶
New builds the root logger -- readable text in development, JSON everywhere else, so it reaches the aggregator without fragile parsing. Middleware puts it at the top of the HTTP pipeline, For reads it back inside a handler or a service, and With attaches fields that everything downstream inherits. There is no exported global logger, on purpose: a log line without request_id and tenant is noise, and the only way to guarantee both is to force the context through.
ParseLevel reads the eight level names and New renders all eight back under those names. Capture is the same logger writing into memory, which is what a test asserts against.
Logger and LogManager ¶
Logger is the eight levels under their own names -- Emergency, Alert, Critical, Error, Warning, Notice, Info, Debug -- plus Log and Write for an arbitrary level, WithContext and WithoutContext for the context every future line carries, and Listen for the MessageLogged event each written line fires.
LogManager is the channels: Channel and Driver resolve one by name out of Config and cache it, Stack fans one line out to several, Build makes a channel the configuration does not name, Extend registers a driver the manager does not know, and ShareContext gives every channel the same fields. It is a logger itself, writing to the default channel.
Both are handed what they need rather than reaching for it: the configuration as a Config, the event dispatcher as the Dispatcher interface.
A failure returns an error, and the emergency logger alongside it, so that a broken logging configuration does not take the request down with it: a caller who wants to keep logging ignores the error, and a caller who wants to know reads it.
A channel is a handler ¶
A channel is a slog.Handler, a stack is a handler that writes into several, and the context processor is a handler that wraps another -- which is what log/context.ContextLogProcessor is. A formatter is the ChannelConfig.Format field, which selects between the two handlers slog ships.
The eight drivers a channel can name are single, daily, monthly, stack, stderr, errorlog, null and custom.
Context ¶
The subpackage log/context is the context that crosses a whole request and lands in every line of it: Add, AddHidden, Push, Pop, Pull, Only, Except, Increment, Scope, Dehydrate. The repository lives in the context.Context, and that package's documentation says why.
The Collector ¶
The Collector is the development layer, and it is core rather than a plugin, so the error page knows the queries, the dumps and the routes without any extra install. It accumulates everything that happened inside one request; Recorder keeps the last of them in a ring; Console serves them at /_arandu/debug and says the probable cause out loud -- the N+1, the slow statement, the request that was three quarters database. Transport records outbound calls onto the same timeline, Dump records a value without writing to stdout or corrupting the response, and EditorLink turns a recorded frame into a link that opens the file in the IDE at the line.
It costs nothing when it is off. Outside development, and without an authorized tracing header, FromContext returns nil and every Record method is a no-op on a nil receiver -- zero cost, not "low cost", and there are allocation tests that hold it to that.
DumpDie is the one exception, and deliberately: the recording half does nothing without a Collector, and the die half runs everywhere. A forgotten call ends the request wherever it is made, because the alternative is a 200 with the dump written into the middle of the page.
The error page itself is not here: it renders a failure rather than recording one, and it lives in the exception package.
Index ¶
- Constants
- Variables
- func Client(timeout time.Duration) *http.Client
- func Dump(ctx context.Context, label string, value any)
- func DumpDie(ctx context.Context, label string, value any)
- func EditorLink(editor, file string, line int, rewrite ...PathRewrite) string
- func Field(key string, value any) slog.Attr
- func Fields(ctx context.Context) []slog.Attr
- func For(ctx context.Context) *slog.Logger
- func Into(ctx context.Context, l *slog.Logger) context.Context
- func IsDumpDie(v any) bool
- func Middleware(l *slog.Logger) func(http.Handler) http.Handler
- func New(env string, level slog.Level) *slog.Logger
- func ParseLevel(s string) (slog.Level, error)
- func Transport(next http.RoundTripper) http.RoundTripper
- func With(ctx context.Context, args ...any) context.Context
- func WithCollector(ctx context.Context, c *Collector) context.Context
- func WithCollectorSlot(ctx context.Context) context.Context
- type ChannelConfig
- type Collector
- func (c *Collector) Dumps() []DumpRecord
- func (c *Collector) Events() []EventRecord
- func (c *Collector) External() []ExternalRecord
- func (c *Collector) Queries() []QueryRecord
- func (c *Collector) QueryCount() int
- func (c *Collector) QueryTime() time.Duration
- func (c *Collector) RecordEvent(name string, payload any)
- func (c *Collector) RecordExternal(method, url string, status int, d time.Duration)
- func (c *Collector) RecordQuery(sql string, args []any, d time.Duration, rows int, err error)
- func (c *Collector) RecordRender(name string, d time.Duration)
- func (c *Collector) Renders() []RenderRecord
- func (c *Collector) SlowQueries(limit time.Duration) []QueryRecord
- func (c *Collector) SuspectedNPlusOne(threshold int) map[string]int
- func (c *Collector) Timeline(total time.Duration) Timeline
- type Config
- type Console
- type Dispatcher
- type DumpRecord
- type EventRecord
- type ExternalRecord
- type Frame
- type GaugeName
- type Gauges
- type LogManager
- func (m *LogManager) Alert(ctx context.Context, message any, fields ...map[string]any)
- func (m *LogManager) Build(config ChannelConfig) (*Logger, error)
- func (m *LogManager) Channel(channel string) (*Logger, error)
- func (m *LogManager) Critical(ctx context.Context, message any, fields ...map[string]any)
- func (m *LogManager) Debug(ctx context.Context, message any, fields ...map[string]any)
- func (m *LogManager) Driver(driver string) (*Logger, error)
- func (m *LogManager) Emergency(ctx context.Context, message any, fields ...map[string]any)
- func (m *LogManager) Error(ctx context.Context, message any, fields ...map[string]any)
- func (m *LogManager) Extend(driver string, callback func(config ChannelConfig) (*slog.Logger, error)) *LogManager
- func (m *LogManager) FlushSharedContext() *LogManager
- func (m *LogManager) ForgetChannel(driver string)
- func (m *LogManager) GetChannels() map[string]*Logger
- func (m *LogManager) GetDefaultDriver() string
- func (m *LogManager) Info(ctx context.Context, message any, fields ...map[string]any)
- func (m *LogManager) Log(ctx context.Context, level slog.Level, message any, fields ...map[string]any)
- func (m *LogManager) Notice(ctx context.Context, message any, fields ...map[string]any)
- func (m *LogManager) SetDefaultDriver(name string)
- func (m *LogManager) ShareContext(fields map[string]any) *LogManager
- func (m *LogManager) SharedContext() map[string]any
- func (m *LogManager) Stack(channels []string, channel string) (*Logger, error)
- func (m *LogManager) Warning(ctx context.Context, message any, fields ...map[string]any)
- func (m *LogManager) WithoutContext(keys ...string) *LogManager
- type Logger
- func (l *Logger) Alert(ctx context.Context, message any, fields ...map[string]any)
- func (l *Logger) Critical(ctx context.Context, message any, fields ...map[string]any)
- func (l *Logger) Debug(ctx context.Context, message any, fields ...map[string]any)
- func (l *Logger) Emergency(ctx context.Context, message any, fields ...map[string]any)
- func (l *Logger) Error(ctx context.Context, message any, fields ...map[string]any)
- func (l *Logger) GetEventDispatcher() Dispatcher
- func (l *Logger) GetLogger() *slog.Logger
- func (l *Logger) Info(ctx context.Context, message any, fields ...map[string]any)
- func (l *Logger) Listen(callback func(events.MessageLogged)) error
- func (l *Logger) Log(ctx context.Context, level slog.Level, message any, fields ...map[string]any)
- func (l *Logger) Notice(ctx context.Context, message any, fields ...map[string]any)
- func (l *Logger) SetEventDispatcher(dispatcher Dispatcher)
- func (l *Logger) Warning(ctx context.Context, message any, fields ...map[string]any)
- func (l *Logger) WithContext(fields ...map[string]any) *Logger
- func (l *Logger) WithoutContext(keys ...string) *Logger
- func (l *Logger) Write(ctx context.Context, level slog.Level, message any, fields ...map[string]any)
- type PathRewrite
- type QueryRecord
- type Record
- type Recorded
- type Recorder
- type Records
- type RenderRecord
- type Timeline
- type UnknownLevelError
Constants ¶
const ( LevelDebug = slog.LevelDebug LevelInfo = slog.LevelInfo LevelNotice = slog.Level(2) LevelWarning = slog.LevelWarn LevelError = slog.LevelError LevelCritical = slog.Level(12) LevelAlert = slog.Level(16) LevelEmergency = slog.Level(20) )
The eight severity levels, expressed as slog levels.
slog names four of them -- Debug, Info, Warn, Error -- and leaves the numeric space between them open exactly so a package can fill it. A level that parses but logs as "ERROR+4" is a level nobody trusts, so New renders all eight by name.
Warning is another spelling of slog's Warn; it is the same level, not a second one.
const ConsolePath = "/_arandu/debug"
ConsolePath is where the console is mounted.
const DefaultRecorderSize = 200
DefaultRecorderSize is how many requests the console remembers.
Two hundred is enough to cover the reload-look-reload loop of a debugging session and small enough that nobody has to think about the memory. Each entry holds the queries, dumps and events of one request, so a page that issues a hundred queries is the one that costs.
const TracingHeader = "X-Arandu-Trace"
TracingHeader carries the secret that turns tracing on outside development, and that the console requires to answer there.
A constant rather than a string in three places: the middleware reads it, the kernel gates on it, and `aru trace` sends it. Three literals is three chances to change one and not the others.
Variables ¶
var ErrNoDispatcher = errors.New("log: events dispatcher has not been set")
ErrNoDispatcher is what Listen answers on a Logger built without a dispatcher.
The dispatcher is optional, so this is not a broken Logger: it writes every line it is given and fires no event. What it cannot do is register a listener, because there is nothing to register with -- and answering an error there is better than accepting a callback that would never be called.
Functions ¶
func Client ¶
Client returns an http.Client that records what it calls.
The timeout is required rather than optional: http.Client has none by default, and a call with no deadline is how one slow dependency turns into every request of the process hanging.
func Dump ¶
Dump records a value for the debug page. It is the print statement you reach for while chasing something, with the difference that matters: it does not write to stdout and does not corrupt the HTML of the response. The value is recorded in the Collector and shown on the debug page.
In production, where the Collector is nil, it is a no-op.
func DumpDie ¶
DumpDie records the value and aborts the request with the dump page. It panics with a sentinel the Recover middleware recognizes, so the abort travels through the middleware chain instead of ending the process.
The die half is not optional ¶
It panics even when there is no Collector, which is every request outside development. The alternative is a request that answers 200 with the dump written into the middle of the response body -- a page that is broken in a way nothing reports, on a code path somebody forgot about, which is exactly how it stays forgotten.
Aborting is also what the rest of the chain already expects: Recover recognises the sentinel, renders the dump page in development, and outside it logs "dump-and-die sentinel outside development" before answering the error page. A forgotten call fails once, loudly, with the line that did it.
The recording half still needs a Collector, and does nothing without one.
func EditorLink ¶
func EditorLink(editor, file string, line int, rewrite ...PathRewrite) string
EditorLink builds the link that opens a file straight in the IDE, at the line. It returns "" when there is no link to build, and the caller renders the frame without one.
It lives here rather than with the error page because two things need it now -- the error page and the console -- and a second copy is a second place to add the next editor. The editor name comes from the log configuration.
The scheme has to reach the template as template.URL: html/template rewrites an unknown scheme to #ZgotmplZ, which turns every link on the page into a dead one and gives no hint why.
An unset editor gets no link, rather than a guess. Clicking a link into a scheme nothing registered does nothing at all, and it does nothing in a way that reads as "the debug page is broken" rather than "the editor is not configured".
An unknown name is the same answer as an unset one. The table is a closed set, so a name outside it is a typo in the configuration, and a typo that produced a link into a scheme nobody registered would look exactly like a working one.
rewrite is optional and only the first is read; without it the path is used as it was recorded. See PathRewrite for the case it is there for -- a link built inside a container that has to open a file outside it.
func Field ¶
Field is one structured field.
It is slog.Any under a name that says what it is at the call site, and it is what lets a caller pass a typed value where a bare key/value pair would leave the reader counting arguments.
func Fields ¶
Fields returns the fields With attached to this context, in the order they were attached.
A *slog.Logger will not say what it carries, so anything that has to forward the request's fields somewhere that is not slog -- the error page, an outbound header, a test -- has no way to read them back. This is that way.
func For ¶
For is the read side of Into: it returns the request logger, and it never returns nil.
This is the only way to log inside a handler or a service. There is no exported global logger, on purpose: a log line without request_id and tenant is noise, and the only way to guarantee both is to force the context through.
func IsDumpDie ¶
IsDumpDie identifies the sentinel so the Recover middleware renders the dump page instead of treating the panic as a real 500.
func Middleware ¶
Middleware installs the application logger at the very top of the pipeline.
Without it, For(ctx) inside a request falls back to slog.Default(), which ignores the configured handler: production would emit its request lines in the default text format instead of the JSON the aggregator expects, and the level filter from the configuration would not apply either. The Kernel installs this as the outermost middleware, so even a panic in Recover logs correctly.
func New ¶
New returns the root logger, which is what a process has before any configuration has been read: readable text in development, JSON everywhere else, so it reaches the aggregator without fragile parsing.
func ParseLevel ¶
ParseLevel turns a configured level name into a level. The half that reads the configuration and falls back to debug is LogManager's levelLocked.
It accepts the eight names exactly as spelled, and nothing else -- no case folding, no trimming, and no "warn" beside "warning". Two names for one level is two spellings of the same configuration, and the handler renders the level back as "warning", so a second spelling would configure a level that never prints under the name it was written with.
An unknown name is an error rather than a silent fallback: a typo in LOG_LEVEL that quietly restores the default is how a production process ends up logging more than it was told to.
The level returned beside the error is debug, which is what LogManager.levelLocked falls back to for a configuration with no level in it at all: two different defaults for the same case is one of them being wrong wherever the two are read next to each other.
func Transport ¶
func Transport(next http.RoundTripper) http.RoundTripper
Transport records every outbound call on the request's Collector.
Without it, "external" on the timeline is always zero and the console shows nothing about the API the handler waited on -- which is the wrong answer for the request whose 800ms were spent in somebody else's service.
Wrap the transport of the client the application uses:
client := &http.Client{
Timeout: 10 * time.Second,
Transport: log.Transport(nil),
}
It costs nothing in production for the same reason everything else here does: with no Collector in the context, RecordExternal returns on a nil receiver.
func With ¶
With attaches fields to the logger the context carries, and returns the context. Logger.WithContext is the other half of the pair, and it mutates one Logger instead.
It replaces the shape that was written everywhere:
log := log.For(ctx).With("component", "worker", "queue", name)
which decorates a local variable and leaves everything called from there -- the repository, the mailer, the panic handler -- logging without the fields. With puts them in the context, so anything downstream that asks For(ctx) gets them too.
The arguments are slog's: alternating key and value, or a slog.Attr, which is what Field returns.
func WithCollector ¶
WithCollector installs the collector in the context, and fills the slot when one was reserved upstream, so a middleware outside this one can still reach it.
func WithCollectorSlot ¶
WithCollectorSlot reserves a place in the context for a Collector that a middleware further in will create. Recover installs it in development; outside development it is not installed at all, so production pays nothing for it.
Types ¶
type ChannelConfig ¶
type ChannelConfig struct {
// Driver selects the implementation: "single", "daily", "monthly", "stack",
// "stderr", "errorlog", "null", "custom", or a name registered with Extend.
Driver string
// Name is the channel name stamped on every line. Empty falls back to
// Config.Env.
Name string
// Level is the lowest level the channel writes, one of the eight names
// ParseLevel accepts. Empty is "debug".
Level string
// Path is the file the single, daily and monthly drivers write to.
Path string
// Days is how many daily files to keep. Zero means 7. MaxFiles wins over it
// when both are set.
Days int
// MaxFiles is how many rotated files to keep. It is read before Days on the
// daily driver, and it is the only count the monthly driver reads. Zero is
// the default the driver names: 7 files for daily, 3 for monthly.
MaxFiles int
// Channels are the channels a stack fans out to.
Channels []string
// IgnoreExceptions swallows what a stack's handlers report. Every handler is
// written to either way; without it the failures come back joined.
IgnoreExceptions bool
// Format is "text" or "json", the two handlers slog ships. Empty follows
// Config.Env.
Format string
// Writer is where the stderr and errorlog drivers write. Empty means the
// process error output.
Writer io.Writer
// Via is the factory the custom driver calls to build the logger.
Via func(config ChannelConfig) (*slog.Logger, error)
}
ChannelConfig is the configuration of one channel.
type Collector ¶
type Collector struct {
Start time.Time
RequestID string
// contains filtered or unexported fields
}
Collector accumulates everything that happened inside ONE request: queries, dumps, events and outbound HTTP calls.
Cost: the Collector is only installed in the context in development or when the request carries an authorized tracing header. In production, without the header, FromContext returns nil and every Record method is a no-op on a nil receiver -- zero cost, not "low cost".
func FromContext ¶
FromContext is the read side of WithCollector.
FromContext returns the request collector, or nil in production. Every method below is safe on a nil receiver, so callers never need to check.
func NewCollector ¶
NewCollector returns a collector for a request id.
func (*Collector) Dumps ¶
func (c *Collector) Dumps() []DumpRecord
Dumps returns a copy of the recorded dumps.
func (*Collector) Events ¶
func (c *Collector) Events() []EventRecord
Events returns a copy of the recorded application events.
func (*Collector) External ¶
func (c *Collector) External() []ExternalRecord
External returns a copy of the recorded outbound HTTP calls.
func (*Collector) Queries ¶
func (c *Collector) Queries() []QueryRecord
Queries returns a copy of the recorded database calls.
A copy, and under the lock: the caller is usually the console rendering a request that has already finished, but a handler that started a goroutine and did not wait for it is still writing. Handing out the slice would hand out a race.
func (*Collector) QueryCount ¶
QueryCount is how many database calls the request made.
It exists so the common case -- a log line saying how many -- does not copy the whole slice to call len on it.
func (*Collector) RecordEvent ¶
RecordEvent stores one application event.
Guard the call when the payload is a struct value:
if col := log.FromContext(ctx); col != nil {
col.RecordEvent("invoice.paid", invoice)
}
This method is a no-op on a nil receiver, but converting a struct value to `any` allocates at the CALL SITE, before the receiver is ever looked at. So the unguarded form costs one heap allocation per event in production, where nothing will ever read it. A payload that is already a pointer, a map or a string boxes for free and needs no guard.
func (*Collector) RecordExternal ¶
RecordExternal stores one outbound HTTP call.
func (*Collector) RecordQuery ¶
RecordQuery stores one database call. The skip value walks past this method and the database.DB wrapper, so Caller points at the repository, not at the framework.
func (*Collector) RecordRender ¶
RecordRender stores one template render.
The view runtime calls it around every render; anything producing HTML can call it too. The name is what shows on the timeline, so it should be the template, not the function.
func (*Collector) Renders ¶
func (c *Collector) Renders() []RenderRecord
Renders returns a copy of the recorded template renders.
func (*Collector) SlowQueries ¶
func (c *Collector) SlowQueries(limit time.Duration) []QueryRecord
SlowQueries returns the queries at or above the limit. It feeds the "slow query" badge on the debug page.
func (*Collector) SuspectedNPlusOne ¶
SuspectedNPlusOne counts identical statements repeated within the request and returns those at or above the threshold. It is the diagnosis that saves the most time on generated CRUD.
type Config ¶
type Config struct {
// Default names the channel Driver and Channel resolve when asked for none.
Default string
// Env is the application environment, and it decides two things: the name
// stamped on a channel that did not name itself, and the format of a channel
// that did not choose one -- readable text under "dev", JSON everywhere else.
Env string
// Channels are the configured channels, by name.
Channels map[string]ChannelConfig
}
Config is the logging configuration: the default channel and the channels themselves.
A manager is handed it whole rather than resolving it from anywhere.
type Console ¶
type Console struct {
// contains filtered or unexported fields
}
Console serves the request inspector at /_arandu/debug.
It is core rather than a package you install. The reason is the thesis of the product: a framework whose selling point is "the debugger names the probable cause" cannot ship the debugger as an optional dependency that half the projects never add.
It renders with html/template and no assets, like the error page and for the same reason: it has to work when the rest is broken, including when the view build failed.
func NewConsole ¶
NewConsole returns the console over a recorder, drawing the numbers held in gauges under the request list.
One constructor rather than a constructor and a setter, so a console is finished when it returns and there is no half-built one to hand to a router.
gauges may be nil, which is what a process that measures nothing passes. The section is then absent rather than present and empty, because an empty table on a diagnostic page reads as a number that failed to arrive.
type Dispatcher ¶
type Dispatcher interface {
// Dispatch fires the event.
Dispatch(event any)
// Listen registers a listener that receives every event. Selecting one type
// out of them is a type assertion, which is what Logger.Listen wraps.
Listen(listener func(event any))
}
Dispatcher is the slice of an event dispatcher that this package needs: something to fire an event into and something to register a listener with.
It is declared here, on the side that consumes it, rather than imported from the events package, so that one concrete dispatcher can serve every package that fires an event -- a Go method cannot be overloaded, so Dispatch has to take any rather than one concrete event type.
type DumpRecord ¶
type DumpRecord struct {
Label string
Value any
Caller Frame
At time.Duration // offset since the start of the request
}
DumpRecord is one Dump call, with its origin and its offset into the request.
type EventRecord ¶
EventRecord is one application event emitted during the request.
type ExternalRecord ¶
ExternalRecord is one outbound HTTP call.
type GaugeName ¶ added in v0.11.0
type GaugeName struct {
// Metric is what is being measured. The registry never interprets it: the
// caller that sets a name is the one that knows what it counts.
Metric string
// Tenant is whose number it is. Empty is the process as a whole, which is
// what a number that cannot honestly be attributed to a tenant reads as.
Tenant string
}
GaugeName identifies one reading: what is measured, and whose it is.
A comparable struct rather than one formatted string, because a string key has to be taken apart again to answer "every tenant this metric was set for", and a metric or a tenant that contains the separator makes that answer wrong.
type Gauges ¶ added in v0.11.0
type Gauges struct {
// contains filtered or unexported fields
}
Gauges holds the current value of numbers the process owns, one int64 per GaugeName.
It keeps exactly one reading per name. Set replaces what was there, and what was there is gone: no history, no window, no peak, no average and no rate. A reader gets what is true now, and there is nothing to expire, sample or page through.
This is what the Collector and the Recorder are not. Both of those are scoped to one request and drop what they hold when it ends, so a number that belongs to the process rather than to a request fits in neither.
The registry stores; it does not measure. Whatever keeps the number is what writes it here, and it is that writer, not this type, that knows what the number means.
Safe for concurrent use.
func NewGauges ¶ added in v0.11.0
func NewGauges() *Gauges
NewGauges returns an empty registry. A name appears the first time it is Set.
func (*Gauges) Names ¶ added in v0.11.0
Names returns every name the registry currently holds, ordered by metric and then by tenant.
Sorted rather than in map order, because callers draw tables from this and a table that reshuffles itself on every reload is a table nobody can compare two readings of.
type LogManager ¶
type LogManager struct {
// contains filtered or unexported fields
}
LogManager resolves channels by name, caches them, shares context across them, and is itself a logger that writes to the default channel.
A LogManager is safe for concurrent use.
func NewLogManager ¶
func NewLogManager(config Config, dispatcher Dispatcher) *LogManager
NewLogManager returns a manager over config, firing its events on dispatcher.
Both may be zero: a manager with no channels resolves nothing and falls back to the emergency logger, which is also where a missing channel lands.
func (*LogManager) Build ¶
func (m *LogManager) Build(config ChannelConfig) (*Logger, error)
Build returns an on-demand channel from a configuration that is not in Config.
It drops the previously built one first, so two Build calls never hand back the same logger.
func (*LogManager) Channel ¶
func (m *LogManager) Channel(channel string) (*Logger, error)
Channel returns the channel by name, or the default one when the name is empty.
A failure returns the emergency logger and the error both, so a caller that wants to keep logging can ignore the error and a caller that wants to know can read it.
func (*LogManager) Driver ¶
func (m *LogManager) Driver(driver string) (*Logger, error)
Driver returns the channel by name, and is what Channel calls.
func (*LogManager) Emergency ¶
Emergency logs at the emergency level. The eight levels and Log all write to the default channel.
func (*LogManager) Extend ¶
func (m *LogManager) Extend(driver string, callback func(config ChannelConfig) (*slog.Logger, error)) *LogManager
Extend registers a factory for a driver name the manager does not know.
The factory receives only the channel configuration; nothing inside the manager is reachable from it.
func (*LogManager) FlushSharedContext ¶
func (m *LogManager) FlushSharedContext() *LogManager
FlushSharedContext clears the context shared across channels.
func (*LogManager) ForgetChannel ¶
func (m *LogManager) ForgetChannel(driver string)
ForgetChannel drops the resolved channel so the next call builds it again.
func (*LogManager) GetChannels ¶
func (m *LogManager) GetChannels() map[string]*Logger
GetChannels returns every channel resolved so far, by name. It is a copy of the map, and the loggers in it are the live ones.
func (*LogManager) GetDefaultDriver ¶
func (m *LogManager) GetDefaultDriver() string
GetDefaultDriver returns the name of the default channel.
func (*LogManager) Log ¶
func (m *LogManager) Log(ctx context.Context, level slog.Level, message any, fields ...map[string]any)
Log logs at an arbitrary level, on the default channel.
A channel that will not resolve does not silence the line: Driver hands back the emergency logger, and the line goes there.
func (*LogManager) SetDefaultDriver ¶
func (m *LogManager) SetDefaultDriver(name string)
SetDefaultDriver sets the name of the default channel.
func (*LogManager) ShareContext ¶
func (m *LogManager) ShareContext(fields map[string]any) *LogManager
ShareContext adds context that every channel gets, the ones already resolved included.
func (*LogManager) SharedContext ¶
func (m *LogManager) SharedContext() map[string]any
SharedContext returns the context shared across channels and stacks.
It is a copy, so writing to it changes nothing the manager holds.
func (*LogManager) Stack ¶
func (m *LogManager) Stack(channels []string, channel string) (*Logger, error)
Stack returns a new aggregate logger over the named channels.
channel names the stack, and empty falls back to Config.Env. The result is not cached.
func (*LogManager) WithoutContext ¶
func (m *LogManager) WithoutContext(keys ...string) *LogManager
WithoutContext drops the given keys from every resolved channel, or clears them all when given none.
It leaves the shared context alone: the two are separate, and FlushSharedContext is the one that clears the shared half.
type Logger ¶
type Logger struct {
// contains filtered or unexported fields
}
Logger is the eight levels, an accumulated context that every future line carries, and a MessageLogged event per line.
It wraps a *slog.Logger: slog already is the chainable handler, and it is stdlib rather than a dependency.
A Logger is safe for concurrent use.
func NewLogger ¶
func NewLogger(logger *slog.Logger, dispatcher Dispatcher) *Logger
NewLogger returns a Logger writing to logger and firing its events on dispatcher.
dispatcher may be nil: a Logger without one writes lines and fires no event, and Listen then reports ErrNoDispatcher. A nil logger falls back to slog.Default rather than panicking on the first line.
func (*Logger) Alert ¶
Alert logs that action must be taken immediately.
Example: entire website down, database unavailable. This should trigger the alerts and wake you up.
func (*Logger) Critical ¶
Critical logs a critical condition.
Example: application component unavailable, unexpected exception.
func (*Logger) Emergency ¶
Emergency logs that the system is unusable.
slog hands ctx to the handler, which is how the request a line belongs to reaches the output. fields is the structured context of the line, and the variadic takes several maps, merged left to right with the last winning. Both notes hold for the other seven levels and for Log and Write.
func (*Logger) Error ¶
Error logs a runtime error that does not require immediate action but should typically be logged and monitored.
func (*Logger) GetEventDispatcher ¶
func (l *Logger) GetEventDispatcher() Dispatcher
GetEventDispatcher returns the dispatcher, or nil when none was set.
func (*Logger) Listen ¶
func (l *Logger) Listen(callback func(events.MessageLogged)) error
Listen registers a callback for when a log event is fired.
A Logger built without a dispatcher reports ErrNoDispatcher and registers nothing. The callback receives only MessageLogged.
func (*Logger) Log ¶
Log logs a message at an arbitrary level.
level is a slog.Level, which is the value the handler compares against; ParseLevel turns a configured name into one.
func (*Logger) SetEventDispatcher ¶
func (l *Logger) SetEventDispatcher(dispatcher Dispatcher)
SetEventDispatcher sets the dispatcher the Logger fires its events on.
func (*Logger) Warning ¶
Warning logs an exceptional occurrence that is not an error.
Example: use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong.
func (*Logger) WithContext ¶
WithContext adds context to all future logs.
It merges rather than replaces, and returns the receiver so that calls chain. No argument merges nothing and is not an error.
func (*Logger) WithoutContext ¶
WithoutContext drops the given keys from the accumulated context, or drops all of it.
Calling it with no key clears everything; calling it with keys drops exactly those and leaves the rest. A key that is not there is not an error.
type PathRewrite ¶
type PathRewrite struct {
// From is the root the running process sees, "/app" above.
From string
// To is the root the editor sees, "/Users/ana/project" above.
To string
}
PathRewrite translates a path the process sees into the path the editor sees.
It exists for one case: the binary was built and runs in a container, so every recorded frame names /app/handler.go, while the editor is on the machine outside and knows the file as /Users/ana/project/handler.go. Without the translation the link opens nothing, and it opens nothing silently.
type QueryRecord ¶
type QueryRecord struct {
SQL string
Args []any
Duration time.Duration
Rows int
Caller Frame
Err error
}
QueryRecord is one database call, with the file and line that issued it -- which is the field that actually saves time when hunting an N+1.
type Record ¶
type Record struct {
Time time.Time
Level slog.Level
Message string
// Attrs is the line's fields, flattened. A group becomes a dotted prefix on
// the keys inside it, which is what a test wants to write: "user.id", not a
// walk over nested values.
Attrs map[string]any
}
Record is one captured log line.
It is not related to Recorder, which is the ring of finished HTTP requests behind the console. This is a single line, and it exists so a test can assert on what was logged.
type Recorded ¶
type Recorded struct {
RequestID string
Method string
Path string
Status int
// Duration is the wall clock of the whole request, which is what the
// timeline is a breakdown of.
Duration time.Duration
// At is when the request started, so the list can be read in order.
At time.Time
// Collector holds everything recorded during the request. It is the same
// pointer the request used: the request is over, so nothing writes to it
// any more.
Collector *Collector
}
Recorded is one finished request, kept for the console.
func (Recorded) StatusClass ¶
StatusClass groups a status code for the console, so the list can be scanned without reading every number.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder is the ring buffer behind /_arandu/debug.
A ring rather than a growing slice, because the alternative is a debug console that turns a long-running dev server into an out-of-memory kill -- and it would happen at the end of a long session, which is exactly when losing the process costs the most.
Every method is safe on a nil receiver. In production there is no recorder, and the middleware should not have to check.
func NewRecorder ¶
NewRecorder returns a ring buffer of the given size. A size of zero or less uses DefaultRecorderSize.
func (*Recorder) Find ¶
Find returns one request by id.
This is what `aru trace` resolves: you have a request id from a log line or from the X-Request-ID header, and you want everything that happened inside it.
type Records ¶
type Records struct {
// contains filtered or unexported fields
}
Records holds what a captured logger wrote. Every method is safe to call while the logger is still being written to from another goroutine.
func Capture ¶
Capture returns a logger that writes into memory, and the memory.
It is what a test installs with Into when the assertion is about a log line: that the throttle said why it refused, that the relay reported the batch it gave up on. Reading stdout to find that out means parsing text, and asserting on formatted text pins the test to the handler rather than to the behaviour.
It captures every level, including debug: a test that has to lower the level first is a test that fails for the wrong reason.
type RenderRecord ¶
RenderRecord is one template render.
It is what separates "the page is slow because of the database" from "the page is slow because of the view", which are two different afternoons.
type Timeline ¶
type Timeline struct {
Total time.Duration
SQL time.Duration
Render time.Duration
External time.Duration
// Other is what is left: application code, serialization, the framework
// itself. A large Other with few queries is a CPU problem, and that is a
// different investigation.
Other time.Duration
}
Timeline is where a request spent its time.
The question it answers is the first one worth asking about a slow page: was it the database, the rendering, something it called, or the code itself. Without the breakdown, "this page takes 800ms" leads to guessing.
type UnknownLevelError ¶
type UnknownLevelError struct{ Name string }
UnknownLevelError is what ParseLevel returns for a name outside the eight.
func (*UnknownLevelError) Error ¶
func (e *UnknownLevelError) Error() string
Error names the rejected value and lists the eight names that would have been accepted, because the only reason anybody reads this message is to fix the value in the configuration.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
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.
|
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. |
|
events
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. |
|
Package events is the event a written log line fires.
|
Package events is the event a written log line fires. |