Documentation
¶
Overview ¶
Package endpoint is the served side of health checking: an http.Handler answering /healthz, /readyz and /metrics, the last in the Prometheus text exposition format 0.0.4.
It depends on the standard library only — no prometheus/client_golang — so a size-conscious binary can expose metrics without importing a metrics framework. There are no registries and no metric objects: a Collector is a function called on every scrape, which writes the current values of its families to a Writer.
http.Handle("/", endpoint.Handler(endpoint.Options{
Ready: func() error { return db.Ping() },
Collectors: []endpoint.Collector{endpoint.GoRuntime, endpoint.BuildInfo("myapp")},
}))
What a Writer guarantees ¶
Whatever a Collector writes, a scrape stays parseable. Names are checked against the Prometheus data model — metric names must match [a-zA-Z_:][a-zA-Z0-9_:]*, label names [a-zA-Z_][a-zA-Z0-9_]* and must not start with "__" (reserved for Prometheus itself). A call to Counter or Gauge is atomic: if its name, any label name, any label value (which must be valid UTF-8), or any label set (duplicated within a sample, or already written for that family) is wrong, none of its samples are written. The call is recorded instead: Writer.Err reports the first such problem, and the scrape ends with one "# endpoint: skipped ..." comment line per problem, which Prometheus ignores and a person reading the scrape sees.
Writing a family name twice in one scrape — typically from two collectors — merges the samples under a single # HELP and # TYPE if both calls use the same type; the first non-empty help text wins. A second call with the other type is a problem as above, and the first type stands.
Values are not checked: a Counter may be given a negative or decreasing value, and Prometheus will treat it as a reset. Names are not rewritten either: the convention that counter names end in "_total" is the caller's to follow — renaming a metric behind its author's back would break every query written against the name they chose.
Number formatting ¶
+Inf, -Inf and NaN are written as such. A value that is an integer of magnitude at most 2^53 is written in plain decimal (1234567, not 1.234567e+06); every other value in the shortest form that parses back to the same float64 (strconv 'g', -1). Both forms are accepted by Go's strconv.ParseFloat, which is what the Prometheus parsers use; the plain form is chosen because byte and object counts are integers and are easier to read and grep in full, and 2^53 is where float64 stops representing every integer exactly.
Index ¶
Examples ¶
Constants ¶
const MetricsContentType = "text/plain; version=0.0.4; charset=utf-8"
MetricsContentType is the Content-Type of a /metrics response.
Variables ¶
This section is empty.
Functions ¶
func GoRuntime ¶
func GoRuntime(w *Writer)
GoRuntime is a Collector for go_goroutines, go_memstats_heap_alloc_bytes, go_memstats_sys_bytes and go_gc_cycles_total.
The values come from runtime/metrics rather than runtime.ReadMemStats. ReadMemStats stops the world on every call — every goroutine of the process pauses so the runtime can take a consistent snapshot — and a scrape runs every few seconds from every scraper, for the lifetime of the process. runtime/metrics.Read reads these values without stopping the world, and is the interface the Go team maintains going forward.
process_start_time_seconds is deliberately not here: only the program knows when it considers itself started. Write it from your own Collector.
func Handler ¶
Handler serves GET and HEAD on three paths:
- /healthz always answers 200 "ok\n": liveness, the process can answer.
- /readyz answers 200 "ok\n" when Options.Ready returns nil (or is nil), and 503 with the error text otherwise.
- /metrics runs every collector into a fresh Writer and answers 200 with Content-Type MetricsContentType.
Any other method on those paths is 405 with an Allow header; any other path is 404. Paths are matched exactly: mount the handler under a prefix with http.StripPrefix.
A Collector, or Ready, that panics does not take the process down or drop the connection: the panic is recovered and that one request answers 500 with the panic value in the body. A scrape is all or nothing — a partial /metrics would make every series of the failed collector look absent rather than unknown.
Every response carries Cache-Control: no-store, so no proxy answers a probe with a stale verdict.
Example ¶
package main
import (
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"github.com/go-net-health/endpoint"
)
func main() {
var draining atomic.Bool
var served atomic.Int64
h := endpoint.Handler(endpoint.Options{
Ready: func() error {
if draining.Load() {
return errors.New("draining")
}
return nil
},
Collectors: []endpoint.Collector{
// endpoint.GoRuntime and endpoint.BuildInfo("myapp") go here too.
func(w *endpoint.Writer) {
w.Counter("myapp_requests_total", "Requests served.", endpoint.S(float64(served.Load())))
},
},
})
srv := httptest.NewServer(h) // in a program: http.ListenAndServe(":9090", h)
defer srv.Close()
get := func(path string) {
res, err := http.Get(srv.URL + path)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Printf("%s %d %s", path, res.StatusCode, b)
}
served.Add(42)
get("/healthz")
get("/readyz")
draining.Store(true)
get("/readyz")
get("/metrics")
}
Output: /healthz 200 ok /readyz 200 ok /readyz 503 draining /metrics 200 # HELP myapp_requests_total Requests served. # TYPE myapp_requests_total counter myapp_requests_total 42
Types ¶
type Collector ¶
type Collector func(*Writer)
A Collector adds its families to a scrape. It is called on every scrape, from the request goroutine; two scrapes running at once call it concurrently, so it must be safe for concurrent use.
func BuildInfo ¶
BuildInfo returns a Collector for <prefix>_build_info{version,goversion,revision} 1 from debug.ReadBuildInfo.
version is the main module's version ("(devel)" for a build that is not from a tagged module), goversion the Go release that built the binary, and revision the vcs.revision stamped by the go command — empty when the build was not made from a VCS checkout, which Prometheus reads as the label being absent. The build information is read once, when BuildInfo is called. An invalid prefix makes every scrape skip the family, as for any invalid name.
type Options ¶
type Options struct {
// Ready reports whether the process should receive traffic; nil means always ready.
// /readyz answers 200 "ok\n" or 503 with the error text.
Ready func() error
// Collectors are called in order on every scrape of /metrics. Nil
// entries are ignored.
Collectors []Collector
}
Options configures a Handler.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
A Writer writes one scrape in text format 0.0.4.
The zero value is ready to use. Families are buffered and written, in the order their names were first seen, by WriteTo. A Writer is not safe for concurrent use; the Handler gives every scrape its own.
Example ¶
package main
import (
"fmt"
"os"
"github.com/go-net-health/endpoint"
)
func main() {
var w endpoint.Writer
w.Gauge("queue_length", "Jobs waiting, by queue.",
endpoint.S(3, endpoint.L("queue", "thumbnails")),
endpoint.S(0, endpoint.L("queue", `say "hi"`)))
w.Gauge("queue_length", "", endpoint.S(1, endpoint.L("queue", "mail"))) // merged
w.Counter("queue_length", "", endpoint.S(1)) // wrong type: skipped
w.WriteTo(os.Stdout)
fmt.Println(w.Err())
}
Output: # HELP queue_length Jobs waiting, by queue. # TYPE queue_length gauge queue_length{queue="thumbnails"} 3 queue_length{queue="say \"hi\""} 0 queue_length{queue="mail"} 1 # endpoint: skipped counter "queue_length": already written as a gauge endpoint: skipped counter "queue_length": already written as a gauge
func (*Writer) Counter ¶
Counter writes a counter family. See the package documentation for what happens when a name or label is invalid or the family already exists.
func (*Writer) Err ¶
Err returns the first problem recorded by Counter or Gauge, or nil. Every problem also appears as a comment line at the end of the scrape.