ops

package
v0.13.0 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package ops is a self-hosted operations dashboard: errors, slow requests, slow queries, the job queue, cron and health, on a page you run yourself.

OpenTelemetry has won as the wire format and this framework emits it. The unsolved part is that OTel is miserable for a small team: a collector to run, a backend to pay for, and a bill that is famously not small. This is the other end of that -- the twenty percent of observability that answers "is it broken and why", with no ingestion, no retention and nothing to pay for.

It is not a competitor to anything. It is the thing that means a solo developer does not need a $200/month dependency to find out that the cron entry stopped firing three weeks ago.

Mounting it

recorder := ops.NewRecorder(0)
app.Logging.OTel.TracerProvider().RegisterSpanProcessor(recorder)

panel.AddPage(ops.Page(ops.Config{
    Recorder: recorder,
    Queues:   []*jobs.SQLQueue{queue},
    Health:   database.NewHealthChecker(db, 2*time.Second),
}))

It is a page in the admin panel, which means it inherits the panel's authorizer and the panel's rule that an unauthenticated visitor gets 404. That is deliberate and it is the one hard requirement in the issue this implements: a dashboard that leaks slow queries and error messages to the internet is not a dashboard, it is a reconnaissance endpoint.

What it does not do

No ingestion, no alerting, no long-term storage, nothing with a bill attached. The retained window is however many spans the Recorder holds, and the page says so rather than implying it has seen everything.

Index

Constants

View Source
const DefaultCapacity = 1000

DefaultCapacity is how many finished spans are kept. At roughly 200 bytes of retained summary each, a thousand is a fifth of a megabyte.

View Source
const DefaultRows = 10

DefaultRows is how many entries each panel shows.

Variables

This section is empty.

Functions

func Page

func Page(cfg Config) admin.Page

Page returns the dashboard as an admin page.

The permission to read it is ActionList and to press its buttons is ActionUpdate, so an operator who may look at the queue but not retry jobs is expressible without any configuration here.

Types

type Config

type Config struct {
	// Recorder supplies errors, slow requests and slow queries. Register it on
	// the tracer provider; see NewRecorder.
	Recorder *Recorder

	// Queues are the job queues to report on.
	Queues []*jobs.SQLQueue

	// Workflows shows which step a durable workflow is stuck on.
	Workflows *jobs.Workflows

	// Health is the database health check.
	Health *database.HealthChecker

	// Cron reports the last run of each scheduled job. See CronReporter.
	Cron CronReporter

	// Title overrides the page heading.
	Title string

	// Rows caps each table. Zero means DefaultRows.
	Rows int
}

Config is what the dashboard has been given to look at.

Every field is optional. A panel with nothing configured says so, per panel, rather than showing an empty table that looks like good news.

type CronFunc

type CronFunc func() []CronRun

CronFunc adapts a function.

func (CronFunc) CronStatus

func (f CronFunc) CronStatus() []CronRun

type CronReporter

type CronReporter interface {
	CronStatus() []CronRun
}

CronReporter reports scheduled-job runs.

This package does not import the framework root, so it cannot name *tjo.BackgroundService. CronRun has the same fields as tjo.CronRun in the same order, which makes a direct struct conversion legal and the adapter one line:

Cron: ops.CronFunc(func() []ops.CronRun {
    runs := app.Background.CronStatus()
    out := make([]ops.CronRun, 0, len(runs))
    for _, r := range runs {
        out = append(out, ops.CronRun(r))
    }
    return out
}),

Six lines in the application beats this package depending on the whole framework, and it keeps the dashboard usable from a program that is not a Tjo application at all.

type CronRun

type CronRun struct {
	Name      string
	LastRun   time.Time
	Duration  time.Duration
	Runs      int
	Failures  int
	LastError string
}

CronRun is the last run of a scheduled job. It mirrors tjo.CronRun field for field, deliberately.

type ErrorGroup

type ErrorGroup struct {
	Name    string
	Message string
	Count   int
	Last    time.Time
}

ErrorGroup is one kind of error and how often it happened.

type Recorder

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

Recorder keeps the most recent finished spans.

It implements sdktrace.SpanProcessor, so it is registered on the tracer provider the otel module already builds:

recorder := ops.NewRecorder(0)
provider.TracerProvider().RegisterSpanProcessor(recorder)

func NewRecorder

func NewRecorder(capacity int) *Recorder

NewRecorder returns a recorder holding capacity spans. Zero means DefaultCapacity.

func (*Recorder) Count

func (r *Recorder) Count() (held, capacity int)

Count reports how many spans are held, and the capacity.

Shown on the dashboard, because "no errors in the last 1000 spans" and "no errors ever" are different claims and only one of them is true.

func (*Recorder) Errors

func (r *Recorder) Errors(limit int) []ErrorGroup

Errors returns the failures, grouped rather than listed.

Grouped because a list of five hundred identical timeouts is not information. The count and the last occurrence are.

func (*Recorder) ForceFlush

func (r *Recorder) ForceFlush(context.Context) error

ForceFlush implements sdktrace.SpanProcessor. There is nothing to flush: the buffer is the destination.

func (*Recorder) HasSpansOfKind

func (r *Recorder) HasSpansOfKind(kind trace.SpanKind) bool

HasSpansOfKind reports whether anything of a kind has been seen.

The difference between "no slow queries" and "the database is not instrumented" is the difference between a green panel and a broken one, and the dashboard says which it is.

func (*Recorder) OnEnd

func (r *Recorder) OnEnd(s sdktrace.ReadOnlySpan)

OnEnd records a finished span.

func (*Recorder) OnStart

OnStart implements sdktrace.SpanProcessor and does nothing: a span is only interesting once it has a duration and an outcome.

func (*Recorder) Shutdown

func (r *Recorder) Shutdown(context.Context) error

Shutdown implements sdktrace.SpanProcessor.

func (*Recorder) SlowQueries

func (r *Recorder) SlowQueries(limit int) []Timing

SlowQueries returns the slowest database spans held.

func (*Recorder) SlowRequests

func (r *Recorder) SlowRequests(limit int) []Timing

SlowRequests returns the slowest HTTP spans held.

type Timing

type Timing struct {
	Name     string
	Detail   string
	Duration time.Duration
	At       time.Time
}

Timing is one slow operation.

Jump to

Keyboard shortcuts

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