uptime

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 14 Imported by: 0

README

uptime

License    Go Version    CI    Go Report Card   

English | 中文

Tiny uptime history middleware for Go net/http.

  • Records heartbeat samples in the background
  • Shows daily uptime bars for the last N days
  • Uses SQLite for single-machine deployments, or PostgreSQL for shared multi-instance deployments
  • Works without Prometheus, Grafana, or an external monitor
  • Complements gofurry/monitor: monitor shows current runtime state, uptime shows historical availability

uptime dashboard preview

Install

go get github.com/gofurry/uptime

Quick Start

package main

import (
	"log"
	"net/http"

	"github.com/gofurry/uptime"
	"github.com/gofurry/uptime/store/sqlite"
)

func main() {
	up, err := uptime.New(uptime.Config{
		ServiceID:   "demo-api",
		ServiceName: "Demo API",
		Store: sqlite.New(sqlite.Config{
			Path: "./uptime.db",
		}),
	})
	if err != nil {
		log.Fatal(err)
	}
	defer up.Close()

	mux := http.NewServeMux()
	mux.Handle("/uptime", up.Handler())
	mux.Handle("/uptime/", up.Handler())
	mux.Handle("/", up.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("hello"))
	})))

	log.Fatal(http.ListenAndServe(":8080", mux))
}

Open:

  • http://localhost:8080/uptime
  • http://localhost:8080/uptime/api/status

Fiber

uptime is built on net/http. Fiber is based on fasthttp, so the safest integration is to create one uptime instance during startup and expose the uptime handler through Fiber's official adaptor.

Important: do not call uptime.New inside a Fiber handler. Each Uptime instance opens the store and starts a background heartbeat goroutine.

package main

import (
	"log"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/adaptor"
	"github.com/gofurry/uptime"
	"github.com/gofurry/uptime/store/sqlite"
)

func main() {
	up, err := uptime.New(uptime.Config{
		ServiceID:   "demo-api",
		ServiceName: "Demo API",
		Store: sqlite.New(sqlite.Config{
			Path: "./uptime.db",
		}),
	})
	if err != nil {
		log.Fatal(err)
	}
	defer up.Close()

	app := fiber.New()
	uptimeHandler := adaptor.HTTPHandler(up.Handler())
	app.All("/uptime", uptimeHandler)
	app.All("/uptime/*", uptimeHandler)

	app.Get("/", func(c *fiber.Ctx) error {
		return c.SendString("hello")
	})

	log.Fatal(app.Listen(":8080"))
}

Open http://localhost:8080/uptime.

The /uptime/* route is needed for /uptime/api/status, which is used by the dashboard refresh and by custom clients. uptime records service availability from its own heartbeat ticker, so you do not need to wrap every Fiber business route.

Demo Data

Generate a local uptime.db with multiple services, multiple instances, and 90 days of history:

go run ./cmd/uptime-demo-data -path ./uptime.db -reset=true
go run ./examples/basic

The example service writes its own heartbeat while the dashboard reads every service stored in the same database file.

Dashboard Only

uptime does not depend on business requests. Heartbeats are written by a background ticker, so this is valid:

mux.Handle("/uptime", up.Handler())
mux.Handle("/uptime/", up.Handler())

Middleware is a pass-through adapter. It is provided for normal net/http integration style and future request-aware features.

PostgreSQL

Use store/postgres when multiple service instances need to share one central uptime database:

package main

import (
	"log"
	"net/http"

	"github.com/gofurry/uptime"
	"github.com/gofurry/uptime/store/postgres"
)

func main() {
	up, err := uptime.New(uptime.Config{
		ServiceID:   "demo-api",
		ServiceName: "Demo API",
		Store: postgres.New(postgres.Config{
			Host:        "127.0.0.1",
			Port:        5432,
			Database:    "postgres",
			Username:    "postgres",
			Password:    "password",
			SSLMode:     "disable",
			Schema:      "public",
			TablePrefix: "uptime_",
		}),
	})
	if err != nil {
		log.Fatal(err)
	}
	defer up.Close()

	mux := http.NewServeMux()
	mux.Handle("/uptime", up.Handler())
	mux.Handle("/uptime/", up.Handler())
	log.Fatal(http.ListenAndServe(":8080", mux))
}

You can also pass postgres.Config{DSN: "postgres://..."}. The PostgreSQL store creates its schema, tables, and indexes automatically. The default table names are uptime_services, uptime_instances, uptime_samples, uptime_daily, and uptime_alert_state; use TablePrefix or Tables for custom names.

Alert Hook

Alerts are optional and disabled by default. Configure Alert.Hook to receive deduplicated service status transitions:

up, err := uptime.New(uptime.Config{
	ServiceID: "dashboard",
	Store:    store,
	Alert: uptime.AlertConfig{
		Hook: func(ctx context.Context, event uptime.AlertEvent) error {
			log.Printf("%s changed from %s to %s", event.ServiceID, event.PreviousStatus, event.CurrentStatus)
			return nil
		},
	},
})

Built-in SQLite and PostgreSQL stores persist alert state, so when several instances share one store only one instance claims a given status transition. The first observed state seeds the alert state and does not notify by default; set NotifyOnFirstDown if an already-down service should notify on first observation.

The hook is for delivery only. Send Slack, email, webhooks, or custom messages from user code.

External Probe

Core uptime records in-process heartbeats. External HTTP checks live in the optional probe package:

p, err := probe.New(probe.Config{
	ServiceID:      "homepage-probe",
	ServiceName:    "Homepage",
	URL:            "https://example.com/health",
	ExpectedStatus: []int{http.StatusOK},
	Interval:       30 * time.Second,
	Timeout:        5 * time.Second,
	Store:          store,
})
if err != nil {
	log.Fatal(err)
}
defer p.Close()

A successful probe writes a heartbeat for its synthetic service. A failed probe writes nothing, so missing slots naturally appear as downtime in the existing dashboard.

Snapshots and Custom UI

The built-in dashboard and JSON API use CachedSnapshot to avoid querying the store on every request. You can use the same API to build your own page or copy the status into Redis, Memcached, or another application cache:

snapshot, err := up.CachedSnapshot(r.Context())
if err != nil {
	http.Error(w, "uptime unavailable", http.StatusInternalServerError)
	return
}

_ = json.NewEncoder(w).Encode(snapshot)

Use Snapshot(ctx) when you explicitly need a fresh store read:

fresh, err := up.Snapshot(ctx)

Snapshot and CachedSnapshot return the same structure as /uptime/api/status.

Configuration

ServiceID and Store are required. The core package does not import SQLite automatically.

Defaults:

Field Default
SampleInterval 3 * time.Second
RetentionDays 90
DaysToShow 90
Timezone time.Local
Snapshot.CacheTTL SampleInterval
Snapshot.DisableCache false
Snapshot.DisableStaleIfError false
UI.Title GoFurry Uptime
UI.Description Historical uptime for Go services sharing this storage.
UI.Footer Powered by github.com/gofurry/uptime - MIT License.
UI.DefaultTheme dark
UI.DefaultLanguage en
UI.Background solid
green threshold 99%
yellow threshold 95%

ServiceID should be a stable business identity such as api, worker, or gofurry-api. Do not generate a new service ID on each start, or history will be split across services.

How It Works

The process writes one up heartbeat every sample interval. Missing heartbeat slots are treated as downtime.

For a 3 second interval:

expected slots per normal day = 24 * 60 * 60 / 3 = 28800
uptime rate = distinct up slots / expected slots

For multiple instances of the same service, a slot is up when any instance writes a heartbeat for that slot.

When multiple services share the same store, the dashboard/API calculate current status, today's expected slots, missing-day expected slots, and estimated downtime with each service's stored sample interval.

Raw samples are kept for today and yesterday. Older samples are rolled up into daily snapshots and then cleaned up.

SQLite Notes

The SQLite store uses the pure-Go modernc.org/sqlite driver and configures:

PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 5000;

SQLite is intended for one machine. Multiple local processes may share the same database file, but network filesystems such as NFS are not recommended.

PostgreSQL Notes

The PostgreSQL store uses github.com/jackc/pgx/v5/stdlib through database/sql. It is intended for shared deployments where multiple processes or machines write to the same uptime store.

Configurable PostgreSQL fields:

Field Default
DSN empty
Host localhost
Port 5432
Database postgres
SSLMode disable
Schema public
TablePrefix uptime_
MaxOpenConns 5
MaxIdleConns 2

Username is required when DSN is empty. Tables can override each table name individually.

Security

The dashboard is public by default. Put authentication, IP allowlists, or reverse proxy rules outside this package when the endpoint is exposed beyond trusted networks.

The middleware does not read request bodies, capture response bodies, log sensitive headers, or store request contexts.

Dashboard

The built-in page has no external assets. The frontend is maintained as embedded page.html, style.css, and app.js files under internal/ui, matching the structure used by gofurry/monitor.

It supports light and dark theme modes, plus English and simplified Chinese labels. The last selected theme and language are stored in browser local storage.

Daily bars use a custom hover card instead of the browser's native tooltip. The card is anchored to the active bar and centered below it.

Concurrency

Uptime instances and the SQLite store are safe for concurrent use after construction. Snapshot cache reads are protected by a mutex and return cloned payloads, so caller-side mutation does not affect the internal cache. Runtime heartbeat failures are recorded in memory and shown as degraded storage status; they do not affect business handlers.

Storage Extensibility

SQLite and PostgreSQL stores are provided. Additional databases can be added through the existing Store interface.

Documentation

Index

Constants

View Source
const (
	AlertStatusUp   = "up"
	AlertStatusDown = "down"
)

Variables

View Source
var (
	ErrMissingServiceID = errors.New("uptime: service id is required")
	ErrMissingStore     = errors.New("uptime: store is required")
)

Functions

This section is empty.

Types

type AlertConfig

type AlertConfig struct {
	Hook AlertHook

	CheckInterval     time.Duration
	NotifyOnFirstDown bool
}

AlertConfig controls optional status-transition notifications.

type AlertDecision

type AlertDecision struct {
	Notify         bool
	PreviousStatus string
}

type AlertEvent

type AlertEvent struct {
	ServiceID      string        `json:"service_id"`
	ServiceName    string        `json:"service_name"`
	Description    string        `json:"description,omitempty"`
	PreviousStatus string        `json:"previous_status"`
	CurrentStatus  string        `json:"current_status"`
	LastSeenAt     time.Time     `json:"last_seen_at"`
	DetectedAt     time.Time     `json:"detected_at"`
	SampleInterval time.Duration `json:"sample_interval"`
	DownFor        time.Duration `json:"down_for"`
}

AlertEvent describes one service status transition.

type AlertHook

type AlertHook func(context.Context, AlertEvent) error

AlertHook receives deduplicated service status transitions.

Hooks are optional. They are called after the shared store has claimed the transition, so built-in stores avoid duplicate notifications across multiple processes using the same SQLite or PostgreSQL storage.

type AlertState

type AlertState struct {
	ServiceID         string
	Status            string
	LastSeenAt        time.Time
	CheckedAt         time.Time
	NotifyOnFirstDown bool
}

type AlertStateStore

type AlertStateStore interface {
	ClaimAlertEvent(ctx context.Context, state AlertState) (AlertDecision, error)
}

AlertStateStore persists alert state for cross-instance de-duplication.

type Background

type Background string

Background controls the dashboard page background.

const (
	BackgroundSolid Background = "solid"
	BackgroundGrid  Background = "grid"
)

type CleanupOptions

type CleanupOptions struct {
	DailyBeforeDay   string
	SamplesBeforeDay string
}

type Config

type Config struct {
	ServiceID          string
	ServiceName        string
	ServiceDescription string

	SampleInterval time.Duration
	RetentionDays  int
	DaysToShow     int

	Timezone *time.Location

	NodeID      int64
	InstanceID  int64
	IDGenerator IDGenerator

	Store Store

	Alert    AlertConfig
	Snapshot SnapshotConfig
	UI       UIConfig
	Logger   Logger
}

Config controls an uptime recorder.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the runtime defaults. ServiceID and Store must still be set.

type DailyStatus

type DailyStatus struct {
	ServiceID     string
	Day           string
	UpSlots       int
	ExpectedSlots int
	UptimeRate    float64
	Finalized     bool
}

DailyStatus is a finalized service-level day snapshot.

type DayStatus

type DayStatus struct {
	Day                      string  `json:"day"`
	UptimeRate               float64 `json:"uptime_rate"`
	UpSlots                  int     `json:"up_slots"`
	ExpectedSlots            int     `json:"expected_slots"`
	EstimatedDowntimeSeconds int64   `json:"estimated_downtime_seconds"`
	Finalized                bool    `json:"finalized"`
	HasData                  bool    `json:"has_data"`
	Status                   string  `json:"status"`
}

type Heartbeat

type Heartbeat struct {
	ServiceID  string
	InstanceID int64
	Day        string
	Slot       int64
	SeenAt     time.Time
}

Heartbeat records that one instance was alive during a day slot.

type IDGenerator

type IDGenerator interface {
	NextID() int64
}

IDGenerator generates instance IDs.

type Instance

type Instance struct {
	ID         int64
	ServiceID  string
	Hostname   string
	PID        int
	StartedAt  time.Time
	LastSeenAt time.Time
}

Instance describes one process lifetime of a service.

type Language

type Language string

Language controls the dashboard's initial UI language.

const (
	LanguageEnglish           Language = "en"
	LanguageChineseSimplified Language = "zh-CN"
)

type Logger

type Logger interface {
	Printf(format string, args ...any)
}

Logger is the small logging contract used by uptime.

type QueryDailyOptions

type QueryDailyOptions struct {
	FromDay string
	ToDay   string
}

type QueryTodaySamplesOptions

type QueryTodaySamplesOptions struct {
	Day string
}

type RollupOptions

type RollupOptions struct {
	BeforeDay                  string
	ExpectedSlotsForDay        func(day string) int
	ExpectedSlotsForServiceDay func(serviceID, day string) int
}

type Service

type Service struct {
	ID             string
	Name           string
	Description    string
	CreatedAt      time.Time
	LastSeenAt     time.Time
	SampleInterval time.Duration
}

Service is the logical service identity shown on the dashboard.

type ServiceStatus

type ServiceStatus struct {
	ID                    string      `json:"id"`
	Name                  string      `json:"name"`
	Description           string      `json:"description,omitempty"`
	LastSeenAt            time.Time   `json:"last_seen_at"`
	CurrentStatus         string      `json:"current_status"`
	SampleIntervalSeconds int64       `json:"sample_interval_seconds"`
	Daily                 []DayStatus `json:"daily"`
}

type Snapshot

type Snapshot = StatusResponse

Snapshot is the current uptime status payload used by the JSON API.

type SnapshotConfig

type SnapshotConfig struct {
	// CacheTTL controls how long a cached snapshot is reused. It defaults to
	// SampleInterval. Set DisableCache to true for a direct store query on every
	// CachedSnapshot call.
	CacheTTL time.Duration

	// DisableCache makes CachedSnapshot behave like Snapshot.
	DisableCache bool

	// DisableStaleIfError returns store errors instead of a stale snapshot when
	// a refresh fails and a previous snapshot is available.
	DisableStaleIfError bool
}

SnapshotConfig controls the in-memory snapshot cache used by CachedSnapshot and the built-in dashboard/API handlers.

type StatusResponse

type StatusResponse struct {
	GeneratedAt           time.Time       `json:"generated_at"`
	SampleIntervalSeconds int64           `json:"sample_interval_seconds"`
	Days                  int             `json:"days"`
	Storage               StorageResponse `json:"storage"`
	Services              []ServiceStatus `json:"services"`
}

type StorageResponse

type StorageResponse struct {
	Driver      string     `json:"driver"`
	Status      string     `json:"status"`
	LastError   string     `json:"last_error,omitempty"`
	LastErrorAt *time.Time `json:"last_error_at,omitempty"`
}

type Store

type Store interface {
	Init(ctx context.Context) error

	UpsertService(ctx context.Context, service Service) error
	UpsertInstance(ctx context.Context, instance Instance) error

	WriteHeartbeat(ctx context.Context, heartbeat Heartbeat) error

	RollupDaily(ctx context.Context, options RollupOptions) error
	Cleanup(ctx context.Context, options CleanupOptions) error

	ListServices(ctx context.Context) ([]Service, error)
	QueryDaily(ctx context.Context, options QueryDailyOptions) ([]DailyStatus, error)
	QueryTodaySamples(ctx context.Context, options QueryTodaySamplesOptions) ([]TodaySampleStatus, error)

	Close() error
}

Store persists uptime state. Implementations must be safe for concurrent use.

type Theme

type Theme string

Theme controls the dashboard's initial color theme.

const (
	ThemeLight Theme = "light"
	ThemeDark  Theme = "dark"
)

type TodaySampleStatus

type TodaySampleStatus struct {
	ServiceID string
	Day       string
	UpSlots   int
}

TodaySampleStatus is the current raw service-level summary for one day.

type UIConfig

type UIConfig struct {
	Title       string
	Path        string
	Description string
	Footer      string

	DefaultTheme    Theme
	DefaultLanguage Language
	Background      Background

	GreenThreshold  float64
	YellowThreshold float64

	ShowInstanceDetails bool
}

UIConfig controls the built-in status page.

type Uptime

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

Uptime records service heartbeats and serves uptime history.

func New

func New(config Config) (*Uptime, error)

New initializes the store, writes the first heartbeat, and starts recording.

func (*Uptime) CachedSnapshot

func (u *Uptime) CachedSnapshot(ctx context.Context) (Snapshot, error)

CachedSnapshot returns a status snapshot through Uptime's in-memory cache.

It is intended for dashboards, custom pages, and user-managed cache layers that should not hit the store for every request. When a refresh fails and a previous snapshot exists, CachedSnapshot returns the stale snapshot with degraded storage status unless Snapshot.DisableStaleIfError is enabled.

func (*Uptime) Close

func (u *Uptime) Close() error

Close stops background work and closes the store.

func (*Uptime) Handler

func (u *Uptime) Handler() http.Handler

Handler serves the built-in dashboard and JSON API.

func (*Uptime) LastError

func (u *Uptime) LastError() (error, time.Time)

LastError returns the most recent runtime store error, if any.

func (*Uptime) Middleware

func (u *Uptime) Middleware(next http.Handler) http.Handler

Middleware is a no-op net/http middleware in v0.1.0.

Heartbeats are written by the background recorder, so uptime works even when the middleware is not installed on the business handler.

func (*Uptime) Snapshot

func (u *Uptime) Snapshot(ctx context.Context) (Snapshot, error)

Snapshot queries the store and returns a fresh status snapshot.

Directories

Path Synopsis
cmd
examples
basic command
postgres command
internal
id
ui
store

Jump to

Keyboard shortcuts

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