observability

package
v0.33.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: 5 Imported by: 0

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

View Source
const ConsolePath = hlog.ConsolePath

ConsolePath is where the console is mounted.

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

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

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.

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(editor, file string, line int) string

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

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 Log

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

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

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

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

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

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

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.

func WithLogger

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

WithLogger stores the request-scoped logger in the context.

It is hesape/log.Into. The context key belongs to hesape, so a logger put in by this name is read back by hesape/log.For and the other way round.

Types

type Collector

type Collector = hlog.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

func FromContext(ctx context.Context) *Collector

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

func NewCollector(requestID string) *Collector

NewCollector returns a collector for a request id.

type Console added in v0.4.0

type Console = hlog.Console

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 added in v0.4.0

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

NewConsole returns the console over a recorder and a gauge registry.

A nil registry draws no gauge section, which is what a caller that has no numbers to show passes.

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 Frame

type Frame = hlog.Frame

Frame is a source location.

type GaugeName added in v0.32.0

type GaugeName = hlog.GaugeName

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

type Gauges = hlog.Gauges

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.32.0

func NewGauges() *Gauges

NewGauges returns an empty registry. A name appears the first time it is Set.

type QueryRecord

type QueryRecord = hlog.QueryRecord

QueryRecord is one database call, with the file and line that issued it.

type Recorded added in v0.4.0

type Recorded = hlog.Recorded

Recorded is one finished request, kept for the console.

type Recorder added in v0.4.0

type Recorder = hlog.Recorder

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

func NewRecorder(size int) *Recorder

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.

type Timeline added in v0.4.0

type Timeline = hlog.Timeline

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

Directories

Path Synopsis
Package errorpage renders the development error page.
Package errorpage renders the development error page.

Jump to

Keyboard shortcuts

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