log

package
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 20 Imported by: 0

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

View Source
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.

View Source
const ConsolePath = "/_arandu/debug"

ConsolePath is where the console is mounted.

View Source
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.

View Source
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

View Source
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

func Client(timeout time.Duration) *http.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

func Dump(ctx context.Context, label string, value any)

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

func DumpDie(ctx context.Context, label string, value any)

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(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

func Field(key string, value any) slog.Attr

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

func Fields(ctx context.Context) []slog.Attr

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

func For(ctx context.Context) *slog.Logger

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 Into

func Into(ctx context.Context, l *slog.Logger) context.Context

Into stores the request-scoped logger in the context.

func IsDumpDie

func IsDumpDie(v any) bool

IsDumpDie identifies the sentinel so the Recover middleware renders the dump page instead of treating the panic as a real 500.

func Middleware

func Middleware(l *slog.Logger) func(http.Handler) http.Handler

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

func New(env string, level slog.Level) *slog.Logger

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

func ParseLevel(s string) (slog.Level, error)

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

func With(ctx context.Context, args ...any) context.Context

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

func WithCollector(ctx context.Context, c *Collector) context.Context

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

func WithCollectorSlot(ctx context.Context) context.Context

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

func FromContext(ctx context.Context) *Collector

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

func NewCollector(requestID string) *Collector

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

func (c *Collector) QueryCount() int

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) QueryTime

func (c *Collector) QueryTime() time.Duration

QueryTime is the total time spent in the database during the request.

func (*Collector) RecordEvent

func (c *Collector) RecordEvent(name string, payload any)

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

func (c *Collector) RecordExternal(method, url string, status int, d time.Duration)

RecordExternal stores one outbound HTTP call.

func (*Collector) RecordQuery

func (c *Collector) RecordQuery(sql string, args []any, d time.Duration, rows int, err error)

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

func (c *Collector) RecordRender(name string, d time.Duration)

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

func (c *Collector) SuspectedNPlusOne(threshold int) map[string]int

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.

func (*Collector) Timeline

func (c *Collector) Timeline(total time.Duration) Timeline

Timeline breaks the request duration down by where it was spent.

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

func NewConsole(r *Recorder, editor string, gauges *Gauges) *Console

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.

func (*Console) Handler

func (c *Console) Handler(w http.ResponseWriter, r *http.Request)

Handler serves the list and the detail.

One handler for both, because the router matches the prefix and the id under it, and splitting them would mean two routes to mount and one more thing to forget.

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

type EventRecord struct {
	Name    string
	Payload any
	At      time.Duration
}

EventRecord is one application event emitted during the request.

type ExternalRecord

type ExternalRecord struct {
	Method   string
	URL      string
	Status   int
	Duration time.Duration
}

ExternalRecord is one outbound HTTP call.

type Frame

type Frame struct {
	File string
	Line int
	Func string
}

Frame is a source location.

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

func (g *Gauges) Names() []GaugeName

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.

func (*Gauges) Read added in v0.11.0

func (g *Gauges) Read(name GaugeName) (int64, bool)

Read returns the current value of one name, and whether that name has ever been Set.

The second result is what separates a number sitting at zero from a number nothing has written yet, which on a screen are the same digit and different facts.

func (*Gauges) Set added in v0.11.0

func (g *Gauges) Set(name GaugeName, value int64)

Set records the current value of one name, replacing any value that name already had.

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) Alert

func (m *LogManager) Alert(ctx context.Context, message any, fields ...map[string]any)

Alert logs at the alert level.

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) Critical

func (m *LogManager) Critical(ctx context.Context, message any, fields ...map[string]any)

Critical logs at the critical level.

func (*LogManager) Debug

func (m *LogManager) Debug(ctx context.Context, message any, fields ...map[string]any)

Debug logs at the debug level.

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

func (m *LogManager) Emergency(ctx context.Context, message any, fields ...map[string]any)

Emergency logs at the emergency level. The eight levels and Log all write to the default channel.

func (*LogManager) Error

func (m *LogManager) Error(ctx context.Context, message any, fields ...map[string]any)

Error logs at the error level.

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) Info

func (m *LogManager) Info(ctx context.Context, message any, fields ...map[string]any)

Info logs at the info level.

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) Notice

func (m *LogManager) Notice(ctx context.Context, message any, fields ...map[string]any)

Notice logs at the notice level.

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) Warning

func (m *LogManager) Warning(ctx context.Context, message any, fields ...map[string]any)

Warning logs at the warning level.

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

func (l *Logger) Alert(ctx context.Context, message any, fields ...map[string]any)

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

func (l *Logger) Critical(ctx context.Context, message any, fields ...map[string]any)

Critical logs a critical condition.

Example: application component unavailable, unexpected exception.

func (*Logger) Debug

func (l *Logger) Debug(ctx context.Context, message any, fields ...map[string]any)

Debug logs detailed debug information.

func (*Logger) Emergency

func (l *Logger) Emergency(ctx context.Context, message any, fields ...map[string]any)

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

func (l *Logger) Error(ctx context.Context, message any, fields ...map[string]any)

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) GetLogger

func (l *Logger) GetLogger() *slog.Logger

GetLogger returns the underlying *slog.Logger.

func (*Logger) Info

func (l *Logger) Info(ctx context.Context, message any, fields ...map[string]any)

Info logs an interesting event.

Example: user logs in, SQL logs.

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

func (l *Logger) Log(ctx context.Context, level slog.Level, message any, fields ...map[string]any)

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) Notice

func (l *Logger) Notice(ctx context.Context, message any, fields ...map[string]any)

Notice logs a normal but significant event.

func (*Logger) SetEventDispatcher

func (l *Logger) SetEventDispatcher(dispatcher Dispatcher)

SetEventDispatcher sets the dispatcher the Logger fires its events on.

func (*Logger) Warning

func (l *Logger) Warning(ctx context.Context, message any, fields ...map[string]any)

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

func (l *Logger) WithContext(fields ...map[string]any) *Logger

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

func (l *Logger) WithoutContext(keys ...string) *Logger

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.

func (*Logger) Write

func (l *Logger) Write(ctx context.Context, level slog.Level, message any, fields ...map[string]any)

Write is an alias of Log: both reach writeLog with the same three arguments.

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

func (e Recorded) StatusClass() string

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

func NewRecorder(size int) *Recorder

NewRecorder returns a ring buffer of the given size. A size of zero or less uses DefaultRecorderSize.

func (*Recorder) Find

func (r *Recorder) Find(requestID string) (Recorded, bool)

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.

func (*Recorder) Len

func (r *Recorder) Len() int

Len is how many requests are stored.

func (*Recorder) Recent

func (r *Recorder) Recent(limit int) []Recorded

Recent returns the stored requests, newest first. A limit of zero or less returns all of them.

func (*Recorder) Record

func (r *Recorder) Record(e Recorded)

Record stores a finished request, discarding the oldest when full.

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

func Capture() (*slog.Logger, *Records)

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.

func (*Records) All

func (r *Records) All() []Record

All returns a copy of the captured lines, oldest first.

func (*Records) Len

func (r *Records) Len() int

Len is how many lines were captured.

type RenderRecord

type RenderRecord struct {
	Name     string
	Duration time.Duration
	At       time.Duration
}

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.

func (Timeline) Percent

func (t Timeline) Percent(d time.Duration) int

Percent returns d as a percentage of the total, rounded down. Zero total gives zero rather than a division by zero on a request that finished in under a microsecond.

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.

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.

Jump to

Keyboard shortcuts

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