Documentation
¶
Overview ¶
Package observability 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.
The Collector is that 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.
This package is a bridge. It is removed in v1.0.0; import github.com/arandu-io/hesape/log directly.
The components moved to github.com/arandu-io/hesape, under new names, and this package is now the old names pointing at them. Everything here is answered by one package:
hesape/log the logger, the Collector, the Recorder, the Console, the
outbound transport, Dump and the editor links
The error page is the one thing that did not go with it: it renders a failure rather than recording one, so it went to hesape/exception, and the bridge for it is the errorpage subpackage.
The death date above is what keeps this from being a second way to import one type. Nothing here holds an implementation: where the name and the signature survived the move it is a Go alias, and where the design diverged it is a call through that translates and nothing more.
The four renames, which are the whole of the divergence:
NewLogger hesape/log.New WithLogger hesape/log.Into Log hesape/log.For RootLogger hesape/log.Middleware
Two behaviours changed with the move, and a caller can tell:
- DumpDie now aborts the request everywhere, not only where a Collector exists. The recording half still needs one; the die half no longer waits for it, because a forgotten call that answered 200 with the dump written into the body was a page broken in a way nothing reported.
- EditorLink returns "" for an unset or unknown editor instead of a vscode:// link nothing is registered to open, and it takes an optional path rewrite this package's signature has no room for.
Index ¶
- Constants
- 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) string
- func IsDumpDie(v any) bool
- func Log(ctx context.Context) *slog.Logger
- func NewLogger(env string, level slog.Level) *slog.Logger
- func RootLogger(l *slog.Logger) func(http.Handler) http.Handler
- func Transport(next http.RoundTripper) http.RoundTripper
- func WithCollector(ctx context.Context, c *Collector) context.Context
- func WithCollectorSlot(ctx context.Context) context.Context
- func WithLogger(ctx context.Context, l *slog.Logger) context.Context
- type Collector
- type Console
- type DumpRecord
- type EventRecord
- type ExternalRecord
- type Frame
- type GaugeName
- type Gauges
- type QueryRecord
- type Recorded
- type Recorder
- type RenderRecord
- type Timeline
Constants ¶
const ConsolePath = hlog.ConsolePath
ConsolePath is where the console is mounted.
const DefaultRecorderSize = hlog.DefaultRecorderSize
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 = hlog.TracingHeader
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 ¶
This section is empty.
Functions ¶
func Client ¶ added in v0.6.1
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.
The die half is not conditional, and that is a behaviour change a caller can see. Returning without panicking when there is no Collector -- which is every request outside development -- makes a forgotten call answer 200 with the dump written into the middle of the body, a page broken in a way nothing reports. It aborts wherever it is called, and outside development Recover logs it by name before answering the error page. The recording half still needs a Collector and still does nothing without one.
func EditorLink ¶ added in v0.4.0
EditorLink builds the link that opens a file straight in the IDE, at the line.
It lives here rather than in errorpage 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.
Two things changed with the move, and both are visible from here. The table is hesape's, which knows fourteen editor names rather than four, and an unset or unknown editor now gets "" instead of a vscode:// link that opens nothing for somebody who configured emacs. And hesape takes an optional path rewrite, for a link built inside a container that has to open a file outside it; this signature has no room for one, so it is the case that needs hesape/log.EditorLink directly.
func IsDumpDie ¶
IsDumpDie identifies the sentinel so the Recover middleware renders the dump page instead of treating the panic as a real 500.
func Log ¶
Log returns the request logger. 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.
It is hesape/log.For.
func NewLogger ¶
NewLogger returns the root logger: readable text in development, JSON everywhere else, so it reaches the aggregator without fragile parsing.
It is hesape/log.New. The rename is the only difference; hesape also renders the four PSR-3 levels slog does not name, which a caller sees only in the output.
func RootLogger ¶
RootLogger installs the application logger at the very top of the pipeline.
Without it, Log(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.
It is hesape/log.Middleware, which returns the same func(http.Handler) http.Handler this has always returned.
func Transport ¶ added in v0.6.1
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: observability.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 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 Collector ¶
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".
The alias carries the methods with it, so RecordQuery, SlowQueries, SuspectedNPlusOne and the rest are hesape's and are documented there.
func FromContext ¶
FromContext returns the request collector, or nil in production. Every method on it is safe on a nil receiver, so callers never need to check.
func NewCollector ¶
NewCollector returns a collector for a request id.
type Console ¶ added in v0.4.0
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.
type DumpRecord ¶
type DumpRecord = hlog.DumpRecord
DumpRecord is one Dump call, with its origin and its offset into the request.
type EventRecord ¶
type EventRecord = hlog.EventRecord
EventRecord is one application event emitted during the request.
type ExternalRecord ¶
type ExternalRecord = hlog.ExternalRecord
ExternalRecord is one outbound HTTP call.
type GaugeName ¶ added in v0.32.0
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.32.0
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.
type QueryRecord ¶
type QueryRecord = hlog.QueryRecord
QueryRecord is one database call, with the file and line that issued it.
type Recorder ¶ added in v0.4.0
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 ¶ added in v0.4.0
NewRecorder returns a ring buffer of the given size. A size of zero or less takes the default.
type RenderRecord ¶ added in v0.4.0
type RenderRecord = hlog.RenderRecord
RenderRecord is one template render.