observability

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package observability wires up logging and metrics.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ContextWithLogger

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

ContextWithLogger returns a context carrying the given logger.

func LoggerFrom

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

LoggerFrom returns the request-scoped logger, or the default logger when there is none. It never returns nil, so callers need no nil check and a missing middleware degrades to unattributed logs rather than a panic.

func NewIngestCollector

func NewIngestCollector(stats IngestStats) prometheus.Collector

NewIngestCollector reports the click pipeline's counters.

func NewLimiterCollector

func NewLimiterCollector(limiters map[string]LimiterStats) prometheus.Collector

NewLimiterCollector reports the named limiters' bookkeeping.

None of these is about throttling — linkctrl_rate_limited_total covers that. They answer a different question: is the limiter still able to do its job. A climbing overflow count means it is not, and that failure is otherwise completely silent, because the design choice on a full table is to allow the request.

Disabled limits must be left out of the map by the caller rather than passed as a nil pointer: a nil pointer inside an interface is not a nil interface, so it would be collected as a working limiter reporting zeros — which reads as "enforcing, and nothing to report" instead of "off".

func NewLogger

func NewLogger(c config.Config, w *os.File) *slog.Logger

NewLogger builds the application logger.

JSON in production so logs are machine-readable; text in development because a human is reading them. Every record carries the service name and version, so logs from two versions during a rolling update are distinguishable.

func NewPoolCollector

func NewPoolCollector(pools map[string]*pgxpool.Pool) prometheus.Collector

NewPoolCollector reports on the named pools.

Both pools are labelled separately because the entire point of splitting them is that they saturate independently: the alert worth having is "the redirect pool is exhausted", which an aggregate number hides.

func SetWebPaths added in v0.2.0

func SetWebPaths(paths []string)

SetWebPaths replaces the dashboard path set with the routes the application mux was given.

Called once at boot, before any request is served. Paths arrive as the mux spells them — an exact path like `/login`, or a subtree like `/links/` — and both are reduced to the prefix form this classifier matches on. The root pattern is dropped because `/` is handled explicitly below.

Types

type IngestStats

type IngestStats interface {
	// QueueDepth is the leading indicator for the whole pipeline: it climbs
	// minutes before drops start.
	QueueDepth() int
	Counters() (enqueued, dropped, flushed, failed, batches int64)
}

IngestStats is what the analytics ingester reports about itself.

An interface rather than the concrete type, because observability must not import analytics — analytics is where the click pipeline lives and a cycle through logging would be waiting to happen. The composition root adapts.

type LimiterStats

type LimiterStats interface {
	// Len is tracked keys, which is the memory the limiter is using.
	Len() int
	// Overflows counts requests allowed because the key table was full — the
	// number that says the limiter has stopped limiting.
	Overflows() int64
	// Fallbacks counts decisions this replica made locally because the shared
	// limiter did not answer. Always zero for a limiter with no shared backing.
	Fallbacks() int64
}

LimiterStats is what a rate limiter reports about its own bookkeeping.

An interface for the same reason as IngestStats: observability must not import the packages it observes.

type Metrics

type Metrics struct {
	// contains filtered or unexported fields
}

Metrics is the instrument panel, built once and passed explicitly.

Its own registry rather than prometheus.DefaultRegisterer: a global registry makes two instances in one test process collide, and it lets any dependency that happens to import client_golang publish into our namespace. Passing the struct also means every metric has one obvious definition site.

Every method is nil-safe. Tests and the CLI build routers without metrics, and an instrumentation call site should not have to know whether metrics happen to be enabled.

func NewMetrics

func NewMetrics() *Metrics

NewMetrics builds the registry and registers every collector.

func (*Metrics) Gather

func (m *Metrics) Gather() *prometheus.Registry

Gather exposes the registry for tests.

func (*Metrics) HTTPMiddleware

func (m *Metrics) HTTPMiddleware(next http.Handler) http.Handler

HTTPMiddleware counts and times every request.

Placed outermost, so the numbers include session lookup, CSRF checks and everything else a handler does not control. The redirect surface is also measured in finer detail inside its handler; this one is the outside view.

func (*Metrics) Handler

func (m *Metrics) Handler() http.Handler

Handler serves the scrape endpoint.

This is mounted on the metrics listener, never on the public one: the series below expose queue depths, pool saturation and the shape of traffic, which is operational detail rather than something to publish.

func (*Metrics) ObserveAutomationFiring added in v0.2.0

func (m *Metrics) ObserveAutomationFiring(trigger, outcome string)

ObserveAutomationFiring records one rule firing (M43).

Called once per firing, not once per subject and not once per evaluation: the question this answers is "how much is the scheduler doing on somebody's behalf", and a rule that matched forty links did one thing.

func (*Metrics) ObserveFeedCheck added in v0.2.0

func (m *Metrics) ObserveFeedCheck(result string)

ObserveFeedCheck records one third-party reputation check.

The count is what makes a failing feed observable at all. A check that errors fails open to the built-in tiers by design, so the destination is accepted and nothing in the product's behaviour says the feed stopped answering — an operator who enabled a feed and is relying on it would otherwise find out by noticing nothing was ever refused.

func (*Metrics) ObserveJob

func (m *Metrics) ObserveJob(job string, err error)

ObserveJob records a background job run.

func (*Metrics) ObserveJobSkipped

func (m *Metrics) ObserveJobSkipped(job string)

ObserveJobSkipped records a run that another replica held the lock for.

Counted rather than ignored: on a healthy multi-replica deployment most runs are skips, and a follower that never skips is a follower that never tried.

func (*Metrics) ObserveRedirect

func (m *Metrics) ObserveRedirect(outcome, cache string, d time.Duration)

ObserveRedirect records one short-link request.

Called from the redirect handler with the duration it already measures for the click event, so instrumentation adds a map lookup and a histogram observation — tens of nanoseconds against a 20ms budget.

One measurement caveat, verified rather than assumed: on a Windows host Go's monotonic clock cannot resolve an interval this short, and time.Since returns exactly zero for 100,000 out of 100,000 back-to-back samples. A cache-served redirect therefore lands in the zero bucket, making _sum and any average useless locally. Bucket counts, and so the "fraction under 20ms" ratio the SLO is stated as, remain correct — and the SLO itself is measured on Linux in containers, where the clock has nanosecond resolution.

func (*Metrics) ObserveThrottled

func (m *Metrics) ObserveThrottled(limit string)

ObserveThrottled records one request refused by a rate limit.

The label names the limit — "login", "api", "redirect_404" — not the client. That is what makes the series bounded, and it is also the more useful cut: an operator wants to know that logins are being throttled, and finds out who from the log if it matters.

func (*Metrics) ObserveWebhookDelivery added in v0.2.0

func (m *Metrics) ObserveWebhookDelivery(outcome, status string)

ObserveWebhookDelivery records one delivery attempt (M42).

Both labels come from a closed vocabulary the caller computes: internal/webhook reduces an HTTP code to its class before calling, so nothing user-chosen can reach a label from here. See the metric's definition for why that matters.

func (*Metrics) Register

func (m *Metrics) Register(c prometheus.Collector)

Register adds a collector that reads live state — pool statistics, queue depth — rather than being written to by instrumentation.

func (*Metrics) SetAuditLogBytes added in v0.2.0

func (m *Metrics) SetAuditLogBytes(n int64)

SetAuditLogBytes records the audit log's on-disk size.

A plain gauge rather than a collector that queries at scrape time, because /metrics has to keep answering while the database is unwell — it is the endpoint an operator scrapes to find out that it is. The cost is that the value is up to an hour stale, which does not matter for a series whose whole purpose is a growth trend measured in days.

Set by every replica, not only the job leader. A gauge only the leader wrote would read as zero on every follower, so whether an alert fired would depend on which replica answered the scrape.

func (*Metrics) SetJobStaleness added in v0.2.0

func (m *Metrics) SetJobStaleness(job string, seconds float64)

SetJobStaleness records how long ago a job last succeeded.

Set by every replica, like SetAuditLogBytes and for the same reason: this is an observation of shared state rather than work that must happen once, and a gauge only the leader wrote would make an alert depend on which replica the scrape reached.

type Surface

type Surface string

Surface is the coarse bucket a request belongs to.

Deliberately coarse. A label per URL path would let anyone mint unbounded series by requesting random aliases — the classic way a metrics endpoint becomes the reason a server falls over — and the redirect tree's whole namespace is attacker-chosen. Per-route detail for the API lives in the access log, which is sampled and does not accumulate.

const (
	SurfaceRedirect Surface = "redirect"
	SurfaceAPI      Surface = "api"
	SurfaceWeb      Surface = "web"
	SurfaceStatic   Surface = "static"
	SurfaceOps      Surface = "ops"
)

func ClassifySurface

func ClassifySurface(path string) Surface

ClassifySurface maps a request path to its surface.

Jump to

Keyboard shortcuts

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