Documentation
¶
Overview ¶
Package observability wires up logging and metrics.
Index ¶
- func ContextWithLogger(ctx context.Context, l *slog.Logger) context.Context
- func LoggerFrom(ctx context.Context) *slog.Logger
- func NewIngestCollector(stats IngestStats) prometheus.Collector
- func NewLimiterCollector(limiters map[string]LimiterStats) prometheus.Collector
- func NewLogger(c config.Config, w *os.File) *slog.Logger
- func NewPoolCollector(pools map[string]*pgxpool.Pool) prometheus.Collector
- func SetWebPaths(paths []string)
- type IngestStats
- type LimiterStats
- type Metrics
- func (m *Metrics) Gather() *prometheus.Registry
- func (m *Metrics) HTTPMiddleware(next http.Handler) http.Handler
- func (m *Metrics) Handler() http.Handler
- func (m *Metrics) ObserveAutomationFiring(trigger, outcome string)
- func (m *Metrics) ObserveFeedCheck(result string)
- func (m *Metrics) ObserveJob(job string, err error)
- func (m *Metrics) ObserveJobSkipped(job string)
- func (m *Metrics) ObserveRedirect(outcome, cache string, d time.Duration)
- func (m *Metrics) ObserveThrottled(limit string)
- func (m *Metrics) ObserveWebhookDelivery(outcome, status string)
- func (m *Metrics) Register(c prometheus.Collector)
- func (m *Metrics) SetAuditLogBytes(n int64)
- func (m *Metrics) SetJobStaleness(job string, seconds float64)
- type Surface
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ContextWithLogger ¶
ContextWithLogger returns a context carrying the given logger.
func LoggerFrom ¶
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 ¶
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 ¶
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 ¶
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
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
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 ¶
ObserveJob records a background job run.
func (*Metrics) ObserveJobSkipped ¶
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 ¶
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 ¶
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
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
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
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.
func ClassifySurface ¶
ClassifySurface maps a request path to its surface.