Help us shape the future of Go! Take the 2026 Go Developer Survey

admin

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package admin is the operator surface of jobrund: the Broker that fans job transitions out to subscribers, the /admin/events SSE feed, /admin/stats, the hand-written /metrics text exposer, the depth cache the two read from, and the embedded /admin page.

The package imports internal/job and web only; the worker, scheduler and reaper reach it through the small interfaces they define themselves (worker.EventPublisher is satisfied by *Broker, DurationObserver by *Histogram).

Index

Constants

View Source
const (
	OutcomeSucceeded = "succeeded"
	OutcomeDead      = "dead"
	OutcomeCancelled = "cancelled"
	OutcomeRetried   = "retried"
)

Outcome names counted by the Broker.

View Source
const ContentType = "text/plain; version=0.0.4; charset=utf-8"

ContentType is the Prometheus text exposition media type.

View Source
const DefaultDepthInterval = 5 * time.Second

DefaultDepthInterval is how often the depth cache refreshes.

View Source
const (
	// KeepaliveInterval is how long the feed may be silent before a
	// `: keepalive` comment is sent so proxies and EventSource keep the
	// connection alive.
	KeepaliveInterval = 15 * time.Second
)

Variables

View Source
var DurationBuckets = durationBuckets[:]

DurationBuckets are the explicit upper bounds (seconds) of jobrun_job_duration_seconds; +Inf is implicit.

Functions

func Events

func Events(b *Broker, snapshot func(context.Context) ([]job.DepthRow, error)) http.Handler

Events streams job transitions from b as Server-Sent Events: one `event: snapshot` (the depth table from snapshot) on connect, then `event: job` per transition, with a keepalive comment while idle. It sends no CORS headers itself; Handler wraps it in the shared origin allowlist.

The handler holds the connection open for as long as the client stays, so it MUST be mounted outside any http.TimeoutHandler (whose ResponseWriter cannot flush and whose deadline would cut the stream); it clears the server's per-request write deadline for its own connection. Handler and NewServer mount it correctly.

func Handler

func Handler(d Deps) http.Handler

Handler mounts the admin surface on a fresh mux:

GET /admin         the embedded synthwave page
GET /admin/events  SSE feed (snapshot, then one event per transition)
GET /admin/stats   JSON summary
GET /metrics       Prometheus text
GET /healthz       liveness, "ok"

Nothing here is wrapped in http.TimeoutHandler: /admin/events is a long-lived stream. If you add middleware around this handler, keep timeouts off the events route (or give the http.Server no WriteTimeout, as NewServer does).

func Metrics

func Metrics(cache *DepthCache, counters func() Counters, hist *Histogram, build map[string]string) http.Handler

Metrics serves the queue metrics in Prometheus text format 0.0.4, written by hand on a pooled buffer (no client library). cache may be nil (no depth series), counters may be nil (no counter series), hist may be nil (no histogram). build becomes the labels of jobrun_build_info; "go_version" defaults to runtime.Version() and "version" to "dev".

Series: jobrun_queue_depth{queue,status}, jobrun_queue_oldest_age_seconds{queue} (oldest queued run_at), jobrun_jobs_total{queue,status}, jobrun_dead_total, jobrun_lease_expired_total, jobrun_scheduler_ticks_total, jobrun_job_duration_seconds{queue,type} (histogram), jobrun_depth_age_seconds, jobrun_build_info.

func NewServer

func NewServer(addr string, d Deps) *http.Server

NewServer builds the admin http.Server on addr (docs/PLAN.md: 127.0.0.1:9110) around Handler(d). WriteTimeout is deliberately zero so the SSE stream is not cut; ReadHeaderTimeout and IdleTimeout bound the rest. Stop it with Server.Shutdown in teardown stage 1; SSE connections end with their request contexts.

func Stats

func Stats(d StatsDeps) http.Handler

Stats serves a JSON summary: version, uptime, the cached depth table, SSE subscriber/drop counts, the outcome counters and the pool stats. It sends no CORS headers itself; Handler wraps it in the shared origin allowlist.

Types

type Broker

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

Broker fans job.Event values out to subscribers. Publish never blocks: a subscriber whose buffer is full misses the event and the drop is counted. It also tallies outcomes per queue for /metrics.

func NewBroker

func NewBroker(log *slog.Logger) *Broker

NewBroker builds an empty broker. A nil logger discards.

func (*Broker) Counters

func (b *Broker) Counters() Counters

Counters returns a copy of the outcome tally (Jobs, Dead). LeaseExpired and SchedulerTicks are zero here; the integrator fills them from obs.Snapshot.

func (*Broker) Dropped

func (b *Broker) Dropped() uint64

Dropped returns the total events dropped across all subscribers since start.

func (*Broker) Publish

func (b *Broker) Publish(e job.Event)

Publish delivers e to every subscriber that has room and counts the outcome. It never blocks and is safe from any goroutine.

func (*Broker) Subscribe

func (b *Broker) Subscribe(buf int) (<-chan job.Event, func())

Subscribe registers a subscriber with a buffer of buf events (min 1) and returns its channel and a cancel func. cancel is idempotent; after it returns the channel is closed and Publish no longer sees the subscriber.

func (*Broker) Subscribers

func (b *Broker) Subscribers() int

Subscribers returns the number of live subscriptions.

type Counters

type Counters struct {
	Jobs           map[JobKey]uint64
	Dead           uint64
	LeaseExpired   uint64
	SchedulerTicks uint64
	// LeaseFenced counts handlers whose lease a heartbeat found gone (in-process
	// pool plus leases WorkerService reported lost to remote workers).
	LeaseFenced uint64
}

Counters is what /metrics renders as counters. Jobs and Dead are fed by the Broker from the events it sees; LeaseExpired and SchedulerTicks come from the runtime (obs.Snapshot) and are filled in by the func the integrator wires.

type Deps

type Deps struct {
	Broker *Broker
	// Depth feeds /metrics, /admin/stats and the SSE snapshot. Run it yourself
	// (DepthCache.Run) in the process errgroup.
	Depth *DepthCache
	// Counters is read per scrape; wire it as
	// func() Counters { c := broker.Counters(); s := snap(); c.LeaseExpired = s.LeaseLost; c.SchedulerTicks = s.SchedulerTicks; return c }.
	Counters func() Counters
	// Durations is the histogram the worker feeds through DurationObserver.
	Durations *Histogram
	// Build labels jobrun_build_info (version, go_version, ...).
	Build     map[string]string
	Version   string
	StartedAt time.Time
	// Pool returns worker.Pool.Stats() for /admin/stats.
	Pool func() any
	// AllowedOrigins (ADMIN_CORS_ORIGINS) are the cross-origin pages allowed
	// to read /admin/stats and /admin/events. Empty (the default) sends no
	// Access-Control-Allow-Origin at all: only the same-origin /admin page
	// can read them.
	AllowedOrigins []string
	Log            *slog.Logger
}

Deps is everything the admin surface reads. Only Broker is required; a nil field simply leaves its data out.

type DepthCache

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

DepthCache refreshes the (queue, status) depth table on a ticker and serves the last good value to /metrics, /admin/stats, the SSE snapshot and the Stats RPC, so a scrape never runs the full scan itself. It satisfies server.DepthSource.

func NewDepthCache

func NewDepthCache(fetch func(context.Context) ([]job.DepthRow, error), every time.Duration, log *slog.Logger) *DepthCache

NewDepthCache builds a cache over fetch (normally job.Store.Depth). every <= 0 means DefaultDepthInterval. Nothing runs until Run is called; Depth returns an empty table with a zero time before the first refresh.

func (*DepthCache) Depth

func (c *DepthCache) Depth() ([]job.DepthRow, time.Time)

Depth returns the last good table and when it was taken (zero before the first successful refresh). The slice is shared: callers must not modify it.

func (*DepthCache) Refresh

func (c *DepthCache) Refresh(ctx context.Context) error

Refresh fetches once, synchronously, and returns the fetch error. Use it before serving when the first scrape must not see an empty table.

func (*DepthCache) Run

func (c *DepthCache) Run(ctx context.Context) error

Run refreshes immediately, then every interval, until ctx is done. It only returns ctx.Err(); a failed refresh is logged and the last value is kept.

type DurationObserver

type DurationObserver interface {
	ObserveDuration(queue, typ string, d time.Duration)
}

DurationObserver is what the worker calls when a handler finishes, so jobrun_job_duration_seconds can be exposed without the worker importing this package. *Histogram implements it; cmd/jobrund passes it to the pool.

type Histogram

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

Histogram is a fixed-bucket cumulative histogram per (queue, type). It is written by handler goroutines and read by /metrics; both paths are cheap.

func NewHistogram

func NewHistogram() *Histogram

NewHistogram builds an empty histogram.

func (*Histogram) ObserveDuration

func (h *Histogram) ObserveDuration(queue, typ string, d time.Duration)

ObserveDuration records one job run.

func (*Histogram) Snapshot

func (h *Histogram) Snapshot() []Series

Snapshot returns every series sorted by queue then type.

type JobKey

type JobKey struct {
	Queue string
	// Status is the outcome: succeeded, dead, cancelled or retried.
	Status string
}

JobKey labels one jobrun_jobs_total series.

type Series

type Series struct {
	Key        SeriesKey
	Cumulative []uint64 // len(DurationBuckets); the +Inf bucket equals Count
	Sum        float64
	Count      uint64
}

Series is a read-only snapshot of one (queue, type) histogram with cumulative bucket counts in DurationBuckets order.

type SeriesKey

type SeriesKey struct {
	Queue string
	Type  string
}

SeriesKey labels one histogram series.

type StatsDeps

type StatsDeps struct {
	Depth     *DepthCache
	Broker    *Broker
	Version   string
	StartedAt time.Time
	// Pool returns the worker pool's Stats() (any JSON-marshalable value);
	// nil omits the field. It is `any` so admin does not import internal/worker.
	Pool func() any
	// Counters is the same func /metrics reads; nil omits the field.
	Counters func() Counters
}

StatsDeps is what /admin/stats reports.

Jump to

Keyboard shortcuts

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