MCPcopy Index your code
hub / github.com/cabify/gotoprom

github.com/cabify/gotoprom @v1.1.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release v1.1.0 ↗ · + Follow
76 symbols 193 edges 9 files 21 documented · 28% 1 cross-repo links
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

gotoprom

A Prometheus metrics builder

Build Status Coverage Status GoDoc Mentioned in Awesome Go

gotoprom offers an easy to use declarative API with type-safe labels for building and using Prometheus metrics. It doesn't replace the official Prometheus client but adds a wrapper on top of it.

gotoprom is built for developers who like type safety, navigating the code using IDEs and using a “find usages” functionality, making refactoring and debugging easier at the cost of performance and writing slightly more verbose code.

Motivation

Main motivation for this library was to have type-safety on the Prometheus labels, which are just a map[string]string in the original library, and their values can be reported even without mentioning the label name, just relying on the order they were declared in.

For example, it replaces:

httpReqs := prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
    },
    []string{"code", "method"},
)
prometheus.MustRegister(httpReqs)

// ...

httpReqs.WithLabelValues("404", "POST").Add(42)

With:

var metrics = struct{
    Reqs func(labels) prometheus.Counter `name:"requests_total" help:"How many HTTP requests processed, partitioned by status code and HTTP method."`
}

type labels struct {
    Code   int    `label:"code"`
    Method string `label:"method"`
}

gotoprom.MustInit(&metrics, "http")

// ...

metrics.Reqs(labels{Code: 404, Method: "POST"}).Inc()

This way it's impossible to mess the call by exchanging the order of "POST" & "404" params.

Usage

Define your metrics:

var metrics struct {
    SomeCounter                      func() prometheus.Counter   `name:"some_counter" help:"some counter"`
    SomeHistogram                    func() prometheus.Histogram `name:"some_histogram" help:"Some histogram with default prometheus buckets" buckets:""`
    SomeHistogramWithSpecificBuckets func() prometheus.Histogram `name:"some_histogram_with_buckets" help:"Some histogram with custom buckets" buckets:".01,.05,.1"`
    SomeGauge                        func() prometheus.Gauge     `name:"some_gauge" help:"Some gauge"`
    SomeSummaryWithSpecificMaxAge    func() prometheus.Summary   `name:"some_summary_with_specific_max_age" help:"Some summary with custom max age" max_age:"20m" objectives:"0.50,0.95,0.99"`

    Requests struct {
        Total func(requestLabels) prometheus.Count `name:"total" help:"Total amount of requests served"`
    } `namespace:"requests"`
}

type requestLabels struct {
    Service    string `label:"service"`
    StatusCode int    `label:"status"`
    Success    bool   `label:"success"`
}

Initialize them:

func init() {
    gotoprom.MustInit(&metrics, "namespace")
}

Measure stuff:

metrics.SomeGauge().Set(100)
metrics.Requests.Total(requestLabels{Service: "google", StatusCode: 404, Success: false}).Inc()

Custom metric types

By default, only some basic metric types are registered when gotoprom is intialized: * prometheus.Counter * prometheus.Histogram * prometheus.Gauge * prometheus.Summary

You can extend this by adding more types, for instance, if you want to observe time and want to avoid repetitive code you can create a prometheusx.TimeHistogram:

package prometheusx

import (
    "reflect"
    "time"

    "github.com/cabify/gotoprom"
    "github.com/cabify/gotoprom/prometheusvanilla"
    "github.com/prometheus/client_golang/prometheus"
)

var (
    // TimeHistogramType is the reflect.Type of the TimeHistogram interface
    TimeHistogramType = reflect.TypeOf((*TimeHistogram)(nil)).Elem()
)

func init() {
    gotoprom.MustAddBuilder(TimeHistogramType, RegisterTimeHistogram)
}

// RegisterTimeHistogram registers a TimeHistogram after registering the underlying prometheus.Histogram in the prometheus.Registerer provided
// The function it returns returns a TimeHistogram type as an interface{}
func RegisterTimeHistogram(name, help, namespace string, labelNames []string, tag reflect.StructTag) (func(prometheus.Labels) interface{}, prometheus.Collector, error) {
    f, collector, err := prometheusvanilla.BuildHistogram(name, help, namespace, labelNames, tag)
    if err != nil {
        return nil, nil, err
    }

    return func(labels prometheus.Labels) interface{} {
        return timeHistogramAdapter{Histogram: f(labels).(prometheus.Histogram)}
    }, collector, nil
}

// TimeHistogram offers the basic prometheus.Histogram functionality
// with additional time-observing functions
type TimeHistogram interface {
    prometheus.Histogram
    // Duration observes the duration in seconds
    Duration(duration time.Duration)
    // Since observes the duration in seconds since the time point provided
    Since(time.Time)
}

type timeHistogramAdapter struct {
    prometheus.Histogram
}

// Duration observes the duration in seconds
func (to timeHistogramAdapter) Duration(duration time.Duration) {
    to.Observe(duration.Seconds())
}

// Since observes the duration in seconds since the time point provided
func (to timeHistogramAdapter) Since(duration time.Time) {
    to.Duration(time.Since(duration))
}

So you can later define it as:

var metrics struct {
    DurationSeconds func() prometheusx.TimeHistogram `name:"duration_seconds" help:"Duration in seconds" buckets:".001,.005,.01,.025,.05,.1"`
}

func init() {
    gotoprom.MustInit(&metrics, "requests")
}

And use it as:

// ...
defer metrics.DurationSeconds().Since(t0)
// ...

Replacing metric builders

If you don't like the default metric builders, you can replace the DefaultInitializer with your own one.

Performance

Obviously, there's a performance cost to perform the type-safety mapping magic to the original Prometheus client's API.

In general terms, it takes 3x to increment a counter than with vanilla Prometheus, which is around 600ns (we're talking about a portion of a microsecond, less than a thousandth of a millisecond)

$ go test -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/cabify/gotoprom
BenchmarkVanilla-4      10000000           387 ns/op
BenchmarkGotoprom-4      5000000          1049 ns/op
PASS
ok      github.com/cabify/gotoprom  10.611s

In terms of memory, there's a also a 33% increase in terms of space, and 3x increase in allocations:

$ go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: github.com/cabify/gotoprom
BenchmarkVanilla-4       5000000           381 ns/op         336 B/op          2 allocs/op
BenchmarkGotoprom-4      1000000          1030 ns/op         432 B/op          6 allocs/op
PASS
ok      github.com/cabify/gotoprom  3.369s

This costs are probably assumable in most of the applications, especially when measuring network accesses, etc. which are magnitudes higher.

Extension points exported contracts — how you extend this code

Initializer (Interface)
Initializer represents an instance of the initializing functionality [2 implementers]
init.go
Builder (FuncType)
Builder is a function that registers a metric and provides a function that creates the metric reporter for given values
init.go

Core symbols most depended-on inside this repo

MustAddBuilder
called by 11
init.go
NewInitializer
called by 8
init.go
MustInit
called by 8
default.go
Init
called by 8
default.go
bucketsFromTag
called by 5
prometheusvanilla/builders.go
MustInit
called by 4
init.go
BuildSummary
called by 4
prometheusvanilla/builders.go
objectivesFromTag
called by 4
prometheusvanilla/builders.go

Shape

Function 43
Struct 16
Method 14
FuncType 1
Interface 1
TypeAlias 1

Languages

Go100%

Modules by API surface

metrics_test.go22 symbols
init.go16 symbols
default_test.go10 symbols
prometheusvanilla/builders.go8 symbols
prometheusvanilla/builders_test.go6 symbols
init_test.go6 symbols
default.go5 symbols
metrics_bench_test.go3 symbols

Used by 1 indexed graphs manifest dependencies, hub-wide

For agents

$ claude mcp add gotoprom \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact